Release
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"include": ["src/*.js"],
|
||||
"exclude": ["**/*.spec.js"],
|
||||
"reporter": ["lcov", "text"],
|
||||
"all": false,
|
||||
"cache": true
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
# Contributors Guide
|
||||
|
||||
Interested in contributing? Awesome! Before you do though, please read our
|
||||
[Code of Conduct](https://slackhq.github.io/code-of-conduct). We take it very seriously, and expect that you will as
|
||||
well.
|
||||
|
||||
There are many ways you can contribute! :heart:
|
||||
|
||||
### Bug Reports and Fixes :bug:
|
||||
- If you find a bug, please [create a new issue](https://github.com/slackapi/slack-github-action/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still
|
||||
be reviewed.
|
||||
- Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`.
|
||||
- If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number.
|
||||
- Include tests that isolate the bug and verifies that it was fixed.
|
||||
|
||||
### New Features :bulb:
|
||||
- If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/slackapi/slack-github-action/issues/new).
|
||||
- Issues that have been identified as a feature request will be labelled `enhancement`.
|
||||
- If you'd like to implement the new feature, please wait for feedback from the project
|
||||
maintainers before spending too much time writing the code. In some cases, `enhancement`s may
|
||||
not align well with the project objectives at the time.
|
||||
|
||||
### Tests :mag:, Documentation :books:, Miscellaneous :sparkles:
|
||||
- If you'd like to improve the tests, you want to make the documentation clearer, you have an
|
||||
alternative implementation of something that may have advantages over the way its currently
|
||||
done, or you have any other change, we would be happy to hear about it!
|
||||
- If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind.
|
||||
- If not, [open an Issue](https://github.com/slackapi/slack-github-actiondcjtfkgitihftbgeungvnhgbvkhjtdbiilknucucrfek/issues/new) to discuss the idea first.
|
||||
|
||||
If you're new to our project and looking for some way to make your first contribution, look for
|
||||
Issues labelled `good first contribution`.
|
||||
|
||||
## Requirements
|
||||
|
||||
For your contribution to be accepted:
|
||||
|
||||
- [x] You must have signed the [Contributor License Agreement (CLA)](https://cla.salesforce.com/sign-cla).
|
||||
- [x] The test suite must be complete and pass.
|
||||
- [x] The changes must be approved by code review.
|
||||
- [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number.
|
||||
|
||||
If the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created.
|
||||
|
||||
[Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z)
|
||||
|
||||
## Creating a Pull Request
|
||||
|
||||
1. :fork_and_knife: Fork the repository on GitHub.
|
||||
2. :runner: Clone/fetch your fork to your local development machine. It's a good idea to run the tests just
|
||||
to make sure everything is in order.
|
||||
3. :herb: Create a new branch and check it out.
|
||||
4. :crystal_ball: Make your changes and commit them locally. Magic happens here!
|
||||
5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-16`).
|
||||
6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `master` in this
|
||||
repository.
|
||||
|
||||
## Maintainers
|
||||
|
||||
There are more details about processes and workflow in the [Maintainer's Guide](./maintainers_guide.md).
|
||||
@@ -1,11 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'monthly'
|
||||
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'monthly'
|
||||
@@ -1,49 +0,0 @@
|
||||
### Description
|
||||
|
||||
Describe your issue here.
|
||||
|
||||
### What type of issue is this? (place an `x` in one of the `[ ]`)
|
||||
- [ ] bug
|
||||
- [ ] enhancement (feature request)
|
||||
- [ ] question
|
||||
- [ ] documentation related
|
||||
- [ ] example code related
|
||||
- [ ] testing related
|
||||
- [ ] discussion
|
||||
|
||||
### Requirements (place an `x` in each of the `[ ]`)
|
||||
* [ ] 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).
|
||||
* [ ] I've searched for any related issues and avoided creating a duplicate issue.
|
||||
|
||||
---
|
||||
|
||||
### Bug Report
|
||||
|
||||
Filling out the following details about bugs will help us solve your issue sooner.
|
||||
|
||||
#### Reproducible in:
|
||||
|
||||
package version:
|
||||
|
||||
node version:
|
||||
|
||||
OS version(s):
|
||||
|
||||
#### Steps to reproduce:
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
#### Expected result:
|
||||
|
||||
What you expected to happen
|
||||
|
||||
#### Actual result:
|
||||
|
||||
What actually happened
|
||||
|
||||
#### Attachments:
|
||||
|
||||
Logs, screenshots, screencast, sample project, funny gif, etc.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Maintainers Guide
|
||||
|
||||
This document describes tools, tasks and workflow that one needs to be familiar with in order to effectively maintain
|
||||
this project. If you use this package within your own software as is but don't plan on modifying it, this guide is
|
||||
**not** for you.
|
||||
|
||||
## Tools
|
||||
|
||||
All you need to work with this project is a supported version of [Node.js](https://nodejs.org/en/)
|
||||
(see `package.json` field "engines") and npm (which is distributed with Node.js).
|
||||
|
||||
## Tasks
|
||||
|
||||
### Developing
|
||||
|
||||
Iterate quickly by developing and testing all techniques of this action with a local version of this action using `npm run dev`.
|
||||
Information on setting up and configuring mocked events can be found in [`.github/resources/README.md`](./resources/README.md).
|
||||
|
||||
### 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.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Versioning and Tags
|
||||
|
||||
This project is versioned using [Semantic Versioning](http://semver.org/), particularly in the
|
||||
[npm flavor](https://docs.npmjs.com/getting-started/semantic-versioning). Each release is tagged
|
||||
using git.
|
||||
|
||||
### Fork
|
||||
|
||||
As a maintainer, the development you do will be almost entirely off of your forked version of this repository. The exception to this rule pertains to multiple collaborators working on the same feature, which is detailed in the **Branches** section below.
|
||||
|
||||
### Branches
|
||||
|
||||
`main` is where active development occurs.
|
||||
|
||||
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.
|
||||
|
||||
### Issue Management
|
||||
|
||||
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.
|
||||
|
||||
**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:
|
||||
`bug`, `enhancement`, `question`, `needs feedback`, `docs`, `tests`, or `discussion`.
|
||||
|
||||
Issues are closed when a resolution has been reached. If for any reason a closed issue seems
|
||||
relevant once again, reopening is great and better than creating a duplicate issue.
|
||||
|
||||
## Everything else
|
||||
|
||||
When in doubt, find the other maintainers and ask.
|
||||
@@ -1,8 +0,0 @@
|
||||
### Summary
|
||||
|
||||
Describe the goal of this PR. Mention any related Issue numbers.
|
||||
|
||||
### Requirements (place an `x` in each `[ ]`)
|
||||
|
||||
* [ ] 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).
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"pull_request": {
|
||||
"title": "Bump actions/checkout from 3 to 4",
|
||||
"html_url": "https://github.com/slackapi/slack-github-action/pull/238"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "slackapi/slack-github-action"
|
||||
},
|
||||
"sender": {
|
||||
"login": "dependabot"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# Move this file to .env before starting
|
||||
|
||||
# https://api.slack.com/apps/A0123456789
|
||||
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
|
||||
@@ -1,6 +0,0 @@
|
||||
# Application values
|
||||
.env.*
|
||||
.env
|
||||
!.env.example
|
||||
.slack/apps.json
|
||||
.slack/apps.*.json
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"experiments": ["bolt"],
|
||||
"project_id": "c4805b41-d1ce-4ea0-b297-ed2f8c64c267"
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"text": ":email: A GitHub Action `${{ github.eventName }}` event status is ${{ env.JOB_STATUS }}",
|
||||
"attachments": [
|
||||
{
|
||||
"color": "${{ env.ATTACHMENT_COLOR }}",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Repository",
|
||||
"short": false,
|
||||
"value": "<${{ github.payload.repository.html_url }}|${{ github.payload.repository.full_name }}>"
|
||||
},
|
||||
{
|
||||
"title": "Ref",
|
||||
"short": false,
|
||||
"value": "${{ github.ref }}"
|
||||
},
|
||||
{
|
||||
"title": "Commit",
|
||||
"short": false,
|
||||
"value": "<${{ github.payload.repository.html_url }}/commit/${{ github.sha }}|${{ github.sha }}>"
|
||||
},
|
||||
{
|
||||
"title": "Author",
|
||||
"short": false,
|
||||
"value": "<https://github.com/${{ github.actor }}|${{ github.actor }}>"
|
||||
},
|
||||
{
|
||||
"title": "Workflow",
|
||||
"short": false,
|
||||
"value": "<${{ github.payload.repository.html_url }}/actions/runs/${{ github.runId }}|${{ github.workflow }}>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"_metadata": {
|
||||
"major_version": 2
|
||||
},
|
||||
"display_information": {
|
||||
"name": "slacktion",
|
||||
"description": "an application for developing the Slack GitHub Action"
|
||||
},
|
||||
"features": {
|
||||
"app_home": {
|
||||
"messages_tab_enabled": true,
|
||||
"messages_tab_read_only_enabled": true
|
||||
},
|
||||
"bot_user": {
|
||||
"display_name": "slacktion"
|
||||
}
|
||||
},
|
||||
"oauth_config": {
|
||||
"scopes": {
|
||||
"bot": [
|
||||
"chat:write",
|
||||
"chat:write.public",
|
||||
"files:write",
|
||||
"incoming-webhook",
|
||||
"reactions:write"
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"org_deploy_enabled": true,
|
||||
"incoming_webhooks": {
|
||||
"incoming_webhooks_enabled": true
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
"event_notifications": {
|
||||
"title": "GitHub event notifications",
|
||||
"description": "A new changeset has been discovered",
|
||||
"input_parameters": {
|
||||
"properties": {
|
||||
"author": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel_id": {
|
||||
"type": "slack#/types/channel_id"
|
||||
},
|
||||
"repo_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"event_url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["author", "channel_id", "repo_name", "status", "event_url"]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "0",
|
||||
"function_id": "slack#/functions/send_message",
|
||||
"inputs": {
|
||||
"channel_id": "{{inputs.channel_id}}",
|
||||
"message": ":ship: {{inputs.repo_name}}(@{{inputs.author}}): `{{inputs.status}}` <{{inputs.event_url}}|Check it out here>!"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"type": "webhook",
|
||||
"name": "Post the latest actions from GitHub",
|
||||
"description": "Share event information to a channel",
|
||||
"workflow": "#/workflows/event_notifications",
|
||||
"inputs": {
|
||||
"author": {
|
||||
"value": "{{data.author}}"
|
||||
},
|
||||
"channel_id": {
|
||||
"value": "{{data.channel_id}}"
|
||||
},
|
||||
"repo_name": {
|
||||
"value": "{{data.repo_name}}"
|
||||
},
|
||||
"status": {
|
||||
"value": "{{data.status}}"
|
||||
},
|
||||
"event_url": {
|
||||
"value": "{{data.event_url}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
# Resources during testing and development
|
||||
|
||||
For a quick development experience and fast testing setup, the app needed with
|
||||
this action is configured using the [app manifest][manifest] and can be used in
|
||||
experiments with the [Slack CLI][cli].
|
||||
|
||||
## Overview
|
||||
|
||||
This app showcases all three techniques of sending data into Slack and follows
|
||||
patterns found in the integration tests.
|
||||
|
||||
- **Technique 1** Slack Workflow Builder: Use a Slack webhook trigger to start a
|
||||
workflow in Slack Workflow Builder.
|
||||
- **Technique 2** Slack API method: Call a Slack API method using a token and
|
||||
data provided through the GitHub workflow.
|
||||
- **Technique 3** Incoming webhook: Post a message to a Slack channel using an
|
||||
incoming webhook.
|
||||
|
||||
Configurations for the Slack app and workflow, and the GitHub Actions workflow
|
||||
are found in the following files:
|
||||
|
||||
- Slack app setup: [`.github/resources/.slack/manifest.json`][slacktion]
|
||||
- GitHub Actions steps: [`.github/workflows/develop.yml`][develop]
|
||||
|
||||
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
|
||||
[steps for CI](#testing-in-ci) is useful when using this app in an actual GitHub
|
||||
Actions workflow.
|
||||
|
||||
### Getting started
|
||||
|
||||
Start by gathering credentials of an application equipped for action:
|
||||
|
||||
```sh
|
||||
$ slack install
|
||||
$ slack trigger create # SLACK_WEBHOOK_TRIGGER
|
||||
$ slack deploy # SLACK_BOT_TOKEN and SLACK_INCOMING_WEBHOOK
|
||||
```
|
||||
|
||||
Gather the **webhook trigger** from the output of the `trigger create` command
|
||||
and collect the **bot token** and **incoming webhook** from app settings.
|
||||
|
||||
Where these are stored will depend on the configurations to run and will follow
|
||||
in the next sections.
|
||||
|
||||
Also be sure to add the new bot to a channel, and while keeping track of that
|
||||
**channel ID** for posting messages or errors happen.
|
||||
|
||||
## Running the workflows
|
||||
|
||||
Both setups share the same starting test suite to make sure the similar examples
|
||||
as in the `README.md` are correct, but changes while testing are encouraged!
|
||||
|
||||
The app uses the values stored in `.slack` and can also be adjusted for changing
|
||||
scopes or workflows.
|
||||
|
||||
### Testing in CI
|
||||
|
||||
Run the workflow as a complete CI check for the changes upstream.
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- The credentials collected above
|
||||
- Access to secrets for the repo
|
||||
|
||||
The saved `test.yml` include common workflows from the `README.md` and other
|
||||
code checks including linting and tests.
|
||||
|
||||
Add gathered credentials and [secrets to save][secrets] to the repository being
|
||||
tested. Required values include:
|
||||
|
||||
- `SLACK_BOT_TOKEN`: xoxb-01010101-example
|
||||
- `SLACK_CHANNEL_ID`: C0123456789
|
||||
- `SLACK_INCOMING_WEBHOOK`:
|
||||
https://hooks.slack.com/services/T0123456789/B0123456789/abcdefghijklmnopqrstuvwxyz
|
||||
- `SLACK_WEBHOOK_TRIGGER`:
|
||||
https://hooks.slack.com/triggers/T0123456789/00000000000/abcdefghijklmnopqrstuvwxyz
|
||||
|
||||
### Experimenting for development
|
||||
|
||||
Run the workflow to post messages without pushing changes upstream.
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- The credentials collected above
|
||||
- An installation of [`nektos/act`](https://github.com/nektos/act)
|
||||
- A running instance of [Docker](https://www.docker.com)
|
||||
|
||||
The saved `develop.yml` workflows use the same `webhook` and `method` examples
|
||||
but skip tests that happen during upstream checks:
|
||||
|
||||
```sh
|
||||
$ cp .env.example .env # Create credentials
|
||||
$ vim .env # Update credentials
|
||||
$ cat .env # Reveal credentials
|
||||
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
|
||||
```
|
||||
|
||||
Environment variables and credentials should be set in the created `.env` file
|
||||
for use in workflows and actions.
|
||||
|
||||
Once credentials are configured and workflows updated, the following command
|
||||
runs the workflow using `act` and the above settings:
|
||||
|
||||
```sh
|
||||
$ npm run dev # Test techniques
|
||||
...
|
||||
[Local run/run] 🏁 Job succeeded
|
||||
```
|
||||
|
||||
## Configuring different things
|
||||
|
||||
### Updating the workflow
|
||||
|
||||
The `develop.yml` file contains the workflow used for testing. Updates to these
|
||||
steps can be made to test various functionalities and edge cases.
|
||||
|
||||
The same applies to test workflows in the `test.yml` file! However, some hope of
|
||||
keeping examples in the `README.md` similar to the tests could be neat.
|
||||
|
||||
### Changing secrets
|
||||
|
||||
To use `${{ secrets.* }}` in the workflow, add more environment variable values
|
||||
to the `.env` file or update the secrets saved to the repository.
|
||||
|
||||
### Mocking event payloads
|
||||
|
||||
Different event payloads can be mocked in development with changes to the values
|
||||
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
|
||||
[develop]: ../workflows/develop.yml
|
||||
[manifest]: https://api.slack.com/concepts/manifests
|
||||
[secrets]: https://github.com/slackapi/slack-github-action/settings/secrets/actions
|
||||
[slacktion]: ./.slack/manifest.json
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"runtime": "actions",
|
||||
"hooks": {
|
||||
"deploy": "open https://api.slack.com/apps || true",
|
||||
"get-manifest": "cat ./.slack/manifest.json #"
|
||||
},
|
||||
"config": {
|
||||
"protocol-version": ["default"],
|
||||
"trigger-paths": ["./.slack/webhook-trigger.json"]
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
name: Development run
|
||||
|
||||
# Requires mocking the "public" event to begin this workflow and avoid actual runs
|
||||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows
|
||||
on:
|
||||
public:
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout action
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup the Node runtime for this project
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
node-version-file: .nvmrc
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Package the build
|
||||
run: npm run build
|
||||
- name: Start a workflow with a 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: ${{ github.event.pull_request.html_url }}
|
||||
repo_name: ${{ github.event.repository.full_name }}
|
||||
status: ${{ job.status }}
|
||||
- name: Post a token message into channel
|
||||
id: api
|
||||
uses: ./
|
||||
with:
|
||||
errors: true
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||
text: ":checkered_flag: Action happens at <https://github.com/${{ github.repository }}>"
|
||||
- name: Initiate the deployment sequence
|
||||
id: slack
|
||||
uses: ./
|
||||
with:
|
||||
errors: true
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||
text: ":eyes: Testing started..."
|
||||
attachments:
|
||||
- color: "dbab09"
|
||||
fields:
|
||||
- title: "Status"
|
||||
short: true
|
||||
value: "In Progress"
|
||||
- name: Write starting statistics
|
||||
id: stats
|
||||
uses: ./
|
||||
with:
|
||||
errors: true
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: ${{ steps.slack.outputs.channel_id }}
|
||||
text: "Started at `${{ steps.slack.outputs.time }}`"
|
||||
thread_ts: "${{ steps.slack.outputs.ts }}"
|
||||
- name: Countdown
|
||||
run: sleep 3
|
||||
- name: Launch time is now
|
||||
id: finished
|
||||
uses: ./
|
||||
with:
|
||||
errors: true
|
||||
method: chat.update
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||
ts: "${{ steps.slack.outputs.ts }}"
|
||||
text: ":microscope: Testing finished!"
|
||||
attachments:
|
||||
- color: "28a745"
|
||||
fields:
|
||||
- title: "Status"
|
||||
short: true
|
||||
value: "Completed"
|
||||
- name: Include ending statistics
|
||||
uses: ./
|
||||
with:
|
||||
errors: true
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: ${{ steps.slack.outputs.channel_id }}
|
||||
text: "Finished at `${{ steps.finished.outputs.time }}`"
|
||||
thread_ts: "${{ steps.stats.outputs.thread_ts }}"
|
||||
- name: Celebrate wins
|
||||
uses: ./
|
||||
with:
|
||||
errors: true
|
||||
method: reactions.add
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||
timestamp: ${{ steps.slack.outputs.ts }}
|
||||
name: "eye-in-speech-bubble"
|
||||
- name: Upload this workflow file
|
||||
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/develop.yml
|
||||
filename: action.yml
|
||||
- name: Write one final webhook
|
||||
uses: ./
|
||||
with:
|
||||
errors: true
|
||||
webhook: ${{ secrets.SLACK_INCOMING_WEBHOOK }}
|
||||
webhook-type: incoming-webhook
|
||||
payload: |
|
||||
text: "end."
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published, edited]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and tag a new version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the current code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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/[email protected]
|
||||
with:
|
||||
template: javascript-action
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
@@ -1,232 +0,0 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
steps:
|
||||
- name: "build: checkout the latest changes"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: "build: setup the node runtime"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: "build: install the required dependencies"
|
||||
run: npm ci
|
||||
|
||||
- name: "unit(test): perform lints and formatting checks"
|
||||
run: npm run lint
|
||||
|
||||
- name: "unit(test): perform check of typings"
|
||||
run: npm run check
|
||||
|
||||
- name: "unit(test): perform unit test checks"
|
||||
run: npm test
|
||||
|
||||
- name: "build: package code for distribution"
|
||||
run: npm run build
|
||||
|
||||
- name: "unit(test): upload coverage to CodeCov"
|
||||
uses: codecov/[email protected]
|
||||
with:
|
||||
directory: ./coverage
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- 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 <https://github.com/${{ github.repository }}>"
|
||||
|
||||
- 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/[email protected]
|
||||
with:
|
||||
codecov_token: ${{ secrets.CODECOV_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
extension: js
|
||||
include: src
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
# Build artifacts
|
||||
dist
|
||||
|
||||
# Development dependencies
|
||||
node_modules
|
||||
|
||||
# Extra files
|
||||
.DS_Store
|
||||
|
||||
# Testing remnants
|
||||
coverage
|
||||
@@ -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
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"files": {
|
||||
"ignore": ["*.json"],
|
||||
"ignoreUnknown": true
|
||||
},
|
||||
"formatter": {
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git"
|
||||
}
|
||||
}
|
||||
Vendored
+55804
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1178
File diff suppressed because it is too large
Load Diff
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -1,16 +0,0 @@
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
new_push_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: New push to repo
|
||||
steps:
|
||||
- name: Send GitHub trigger payload to Slack Workflow Builder
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v2-development
|
||||
with:
|
||||
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
webhook-type: webhook-trigger
|
||||
payload: |
|
||||
key: value
|
||||
foo: bar
|
||||
@@ -1,14 +0,0 @@
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
new_push_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: New push to repo
|
||||
steps:
|
||||
- name: Send GitHub trigger payload to Slack Workflow Builder
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v2-development
|
||||
with:
|
||||
payload-file-path: "./example-workflows/Technique_1_Slack_Workflow_Builder/payloads/example.json"
|
||||
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
webhook-type: webhook-trigger
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
new_push_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: New push to repo
|
||||
steps:
|
||||
- name: Send GitHub trigger payload to Slack Workflow Builder
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v2-development
|
||||
with:
|
||||
payload-delimiter: "_"
|
||||
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
webhook-type: webhook-trigger
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"text": "${{ github.workflow }} workflow failed on ${{ env.DEFAULT_BRANCH }} branch in the ${{ github.event.repository.name }} repository!"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
new_push_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: New push to repo
|
||||
steps:
|
||||
- name: Send GitHub trigger payload to Slack Workflow Builder
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v2-development
|
||||
with:
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
method: chat.postMessage
|
||||
payload: |
|
||||
channel: "C0123456789"
|
||||
text: "messages make meeting"
|
||||
blocks:
|
||||
- type: "divider"
|
||||
type: "image"
|
||||
title:
|
||||
type: "plain_text"
|
||||
text: "Slack Slack Slack"
|
||||
emoji: true
|
||||
image_url: "https://media.makeameme.org/created/a-slack-this.jpg"
|
||||
alt_text: "marg"
|
||||
- type: "actions"
|
||||
elements:
|
||||
- type": "button"
|
||||
text:
|
||||
type: "plain_text"
|
||||
text: "${{ github.sha }}"
|
||||
url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
@@ -1,16 +0,0 @@
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
new_push_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: New push to repo
|
||||
steps:
|
||||
- name: Send GitHub trigger payload to Slack Workflow Builder
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v2-development
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
text: "posting from a github action"
|
||||
channel: "C0123456789"
|
||||
@@ -1,16 +0,0 @@
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
new_push_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: New push to repo
|
||||
steps:
|
||||
- name: Publish to slack channel via bot token
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v2-development
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: "C0123456789"
|
||||
text: "posting from a github action!"
|
||||
@@ -1,34 +0,0 @@
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
new_push_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: New push to repo
|
||||
steps:
|
||||
- name: Send GitHub trigger payload to Slack Workflow Builder
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v2-development
|
||||
with:
|
||||
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
webhook-type: webhook-trigger
|
||||
payload: |
|
||||
"text": "Danny Torrence left a 1 star review for your property."
|
||||
"blocks":
|
||||
- type: "section"
|
||||
text:
|
||||
type: "mrkdwn"
|
||||
text: "Danny Torrence left the following review for your property:"
|
||||
- type: "section"
|
||||
block_id: "section567"
|
||||
text: {
|
||||
type: "mrkdwn"
|
||||
text: "<https://example.com|Overlook Hotel> \\n :star: \\n Doors had too many axe holes, guest in room 237 was far too rowdy, whole place felt stuck in the 1920s."
|
||||
accessory:
|
||||
type: "image"
|
||||
image_url: "https://is5-ssl.mzstatic.com/image/thumb/Purple3/v4/d3/72/5c/d3725c8f-c642-5d69-1904-aa36e4297885/source/256x256bb.jpg"
|
||||
alt_text: "Haunted hotel image"
|
||||
- type: "section"
|
||||
block_id: "section789"
|
||||
fields:
|
||||
- type: "mrkdwn"
|
||||
text: "*Average Rating*\\n1.0"
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"ES2023"
|
||||
],
|
||||
"module": "node16",
|
||||
"moduleResolution": "node",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"target": "ES2022"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.js"
|
||||
],
|
||||
"jsdoc": {
|
||||
"out": "support/jsdoc",
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
Generated
-2527
File diff suppressed because it is too large
Load Diff
@@ -1,63 +0,0 @@
|
||||
{
|
||||
"name": "slack-github-action",
|
||||
"version": "2.0.0-development",
|
||||
"description": "The official Slack Github Action. Use this to send data into your Slack workspace",
|
||||
"main": "dist/index.js",
|
||||
"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",
|
||||
"lint": "biome check",
|
||||
"test": "c8 mocha test/*.spec.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/slackapi/slack-github-action.git"
|
||||
},
|
||||
"keywords": [
|
||||
"slack",
|
||||
"github",
|
||||
"action",
|
||||
"workflow"
|
||||
],
|
||||
"author": "Slack Technologies, LLC",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/slackapi/slack-github-action/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=10.2.0"
|
||||
},
|
||||
"homepage": "https://github.com/slackapi/slack-github-action#readme",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@slack/web-api": "^7.7.0",
|
||||
"axios": "^1.7.7",
|
||||
"axios-retry": "^4.5.0",
|
||||
"flat": "^6.0.1",
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markup-js": "^1.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/flat": "^5.0.5",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/markup-js": "^1.5.0",
|
||||
"@types/mocha": "^10.0.9",
|
||||
"@types/node": "^20.17.6",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@vercel/ncc": "^0.38.2",
|
||||
"c8": "^10.1.2",
|
||||
"chai": "^5.1.2",
|
||||
"mocha": "^10.8.2",
|
||||
"mocha-suppress-logs": "^0.5.1",
|
||||
"sinon": "^19.0.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
import webapi, { LogLevel } from "@slack/web-api";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import Config from "./config.js";
|
||||
import SlackError from "./errors.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/}
|
||||
*/
|
||||
export default class Client {
|
||||
/**
|
||||
* Perform the API call configured with the input payload.
|
||||
* @param {Config} config
|
||||
*/
|
||||
async post(config) {
|
||||
if (!config.inputs.method) {
|
||||
throw new SlackError(config.core, "No API method was provided for use");
|
||||
}
|
||||
if (!config.inputs.token) {
|
||||
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,
|
||||
retryConfig: this.retries(config.inputs.retries),
|
||||
logger: {
|
||||
debug: config.core.debug,
|
||||
info: config.core.info,
|
||||
warn: config.core.warning,
|
||||
error: config.core.error,
|
||||
getLevel: () => {
|
||||
return config.core.isDebug() ? LogLevel.DEBUG : LogLevel.INFO;
|
||||
},
|
||||
setLevel: (_level) => {},
|
||||
setName: (_name) => {},
|
||||
},
|
||||
});
|
||||
try {
|
||||
/**
|
||||
* @type {webapi.WebAPICallResult & import("@slack/web-api").ChatPostMessageResponse & import("@slack/web-api").ConversationsCreateResponse}
|
||||
*/
|
||||
const response = await client.apiCall(
|
||||
config.inputs.method,
|
||||
config.content.values,
|
||||
);
|
||||
config.core.setOutput("ok", response.ok);
|
||||
config.core.setOutput("response", JSON.stringify(response));
|
||||
if (response.channel?.id ?? response.channel) {
|
||||
config.core.setOutput(
|
||||
"channel_id",
|
||||
response.channel?.id ?? response.channel,
|
||||
);
|
||||
}
|
||||
if (response.message?.thread_ts) {
|
||||
config.core.setOutput("thread_ts", response.message.thread_ts);
|
||||
}
|
||||
if (response.ts) {
|
||||
config.core.setOutput("ts", response.ts);
|
||||
}
|
||||
} catch (/** @type {any} */ err) {
|
||||
const slackErr = /** @type {webapi.WebAPICallError} */ (err);
|
||||
config.core.setOutput("ok", false);
|
||||
switch (slackErr.code) {
|
||||
case webapi.ErrorCode.RequestError:
|
||||
config.core.setOutput("response", JSON.stringify(slackErr.original));
|
||||
break;
|
||||
case webapi.ErrorCode.HTTPError:
|
||||
config.core.setOutput("response", JSON.stringify(slackErr));
|
||||
break;
|
||||
case webapi.ErrorCode.PlatformError:
|
||||
config.core.setOutput("response", JSON.stringify(slackErr.data));
|
||||
break;
|
||||
case webapi.ErrorCode.RateLimitedError:
|
||||
config.core.setOutput("response", JSON.stringify(slackErr));
|
||||
break;
|
||||
}
|
||||
throw new SlackError(config.core, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {import("@slack/web-api").RetryOptions}
|
||||
*/
|
||||
retries(option) {
|
||||
switch (option?.trim().toUpperCase()) {
|
||||
case "0":
|
||||
return { retries: 0 };
|
||||
case "5":
|
||||
return webapi.retryPolicies.fiveRetriesInFiveMinutes;
|
||||
case "10":
|
||||
return webapi.retryPolicies.tenRetriesInAboutThirtyMinutes;
|
||||
case "RAPID":
|
||||
return webapi.retryPolicies.rapidRetryPolicy;
|
||||
default:
|
||||
return webapi.retryPolicies.fiveRetriesInFiveMinutes;
|
||||
}
|
||||
}
|
||||
}
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
import core from "@actions/core";
|
||||
import webapi from "@slack/web-api";
|
||||
import axios from "axios";
|
||||
import Content from "./content.js";
|
||||
import SlackError from "./errors.js";
|
||||
|
||||
/**
|
||||
* Options and settings set as inputs to this action.
|
||||
*
|
||||
* @see {@link ../action.yml}
|
||||
*/
|
||||
export default class Config {
|
||||
/**
|
||||
* Options of retries for failed requests.
|
||||
* @readonly
|
||||
* @enum {string} The option for retries.
|
||||
*/
|
||||
Retries = {
|
||||
/** No retries, just hope that things go alright.
|
||||
* @readonly
|
||||
*/
|
||||
ZERO: "0",
|
||||
/**
|
||||
* Five retries in five minutes.
|
||||
* @readonly
|
||||
*/
|
||||
FIVE: "5",
|
||||
/**
|
||||
* Ten retries in about thirty minutes.
|
||||
* @readonly
|
||||
*/
|
||||
TEN: "10",
|
||||
/**
|
||||
* A burst of retries to keep things running fast.
|
||||
* @readonly
|
||||
*/
|
||||
RAPID: "RAPID",
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef Inputs - Values provided to this job.
|
||||
* @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?} 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?} 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Inputs} - The actual action input values.
|
||||
*/
|
||||
inputs;
|
||||
|
||||
/**
|
||||
* @type {import("axios").AxiosStatic} - The axios client.
|
||||
*/
|
||||
axios;
|
||||
|
||||
/**
|
||||
* @type {Content} - The parsed payload data to send.
|
||||
*/
|
||||
content;
|
||||
|
||||
/**
|
||||
* Shared utilities specific to the GitHub action workflow.
|
||||
* @type {import("@actions/core")}
|
||||
*/
|
||||
core;
|
||||
|
||||
/**
|
||||
* @type {import("@slack/web-api")} - Slack API client.
|
||||
*/
|
||||
webapi;
|
||||
|
||||
/**
|
||||
* Gather values from the job inputs and use defaults or error for the missing
|
||||
* ones.
|
||||
*
|
||||
* The content of the payload is also parsed, proxies set, and a shared "core"
|
||||
* kept for later use.
|
||||
*
|
||||
* @constructor
|
||||
* @param {core} core - GitHub Actions core utilities.
|
||||
*/
|
||||
constructor(core) {
|
||||
this.axios = axios;
|
||||
this.core = core;
|
||||
this.webapi = webapi;
|
||||
this.inputs = {
|
||||
errors: core.getBooleanInput("errors"),
|
||||
method: core.getInput("method"),
|
||||
payload: core.getInput("payload"),
|
||||
payloadDelimiter: core.getInput("payload-delimiter"),
|
||||
payloadFilePath: core.getInput("payload-file-path"),
|
||||
payloadTemplated: core.getBooleanInput("payload-templated") || false,
|
||||
proxy:
|
||||
core.getInput("proxy") ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy ||
|
||||
null,
|
||||
retries: core.getInput("retries") || this.Retries.FIVE,
|
||||
token: core.getInput("token") || process.env.SLACK_TOKEN || null,
|
||||
webhook:
|
||||
core.getInput("webhook") || process.env.SLACK_WEBHOOK_URL || null,
|
||||
webhookType: core.getInput("webhook-type"),
|
||||
};
|
||||
this.validate();
|
||||
core.debug(`Gathered action inputs: ${JSON.stringify(this.inputs)}`);
|
||||
this.content = new Content().get(this);
|
||||
core.debug(`Parsed request content: ${JSON.stringify(this.content)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the configurations are correct enough to continue.
|
||||
*/
|
||||
validate() {
|
||||
switch (this.inputs.retries.trim().toUpperCase()) {
|
||||
case this.Retries.ZERO:
|
||||
case this.Retries.FIVE:
|
||||
case this.Retries.TEN:
|
||||
case this.Retries.RAPID:
|
||||
break;
|
||||
default:
|
||||
throw new SlackError(
|
||||
core,
|
||||
`Invalid input! An unknown "retries" value was used: ${this.inputs.retries}`,
|
||||
);
|
||||
}
|
||||
switch (true) {
|
||||
case !!this.inputs.token && !!this.inputs.webhook:
|
||||
core.debug(
|
||||
"Setting the provided token and webhook as secret variables.",
|
||||
);
|
||||
core.setSecret(this.inputs.token);
|
||||
core.setSecret(this.inputs.webhook);
|
||||
throw new SlackError(
|
||||
core,
|
||||
"Invalid input! Either the token or webhook is required - not both.",
|
||||
);
|
||||
case !!this.inputs.token:
|
||||
core.debug("Setting the provided token as a secret variable.");
|
||||
core.setSecret(this.inputs.token);
|
||||
if (!this.inputs.method) {
|
||||
throw new SlackError(
|
||||
core,
|
||||
"Missing input! A method must be decided to use the token provided.",
|
||||
);
|
||||
}
|
||||
break;
|
||||
case !!this.inputs.webhook:
|
||||
core.debug("Setting the provided webhook as a secret variable.");
|
||||
core.setSecret(this.inputs.webhook);
|
||||
if (!this.inputs.webhookType) {
|
||||
throw new SlackError(
|
||||
core,
|
||||
"Missing input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.inputs.webhookType !== "incoming-webhook" &&
|
||||
this.inputs.webhookType !== "webhook-trigger"
|
||||
) {
|
||||
throw new SlackError(
|
||||
core,
|
||||
"Invalid input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.",
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new SlackError(
|
||||
core,
|
||||
"Missing input! Either a token or webhook is required to take action.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import github from "@actions/github";
|
||||
import { flatten } from "flat";
|
||||
import yaml from "js-yaml";
|
||||
import markup from "markup-js";
|
||||
import Config from "./config.js";
|
||||
import SlackError from "./errors.js";
|
||||
|
||||
/**
|
||||
* The parsed payload provided to the action and passed to the preferred method
|
||||
* of sending a payload.
|
||||
*/
|
||||
export default class Content {
|
||||
/**
|
||||
* Gather content from the provided payload or payload file path with parsings.
|
||||
* @param {Config} config
|
||||
* @returns {this} - An instance of this Content class.
|
||||
*/
|
||||
get(config) {
|
||||
switch (true) {
|
||||
case !!config.inputs.payload && !!config.inputs.payloadFilePath:
|
||||
throw new SlackError(
|
||||
config.core,
|
||||
"Invalid input! Just the payload or payload file path is required.",
|
||||
);
|
||||
case !!config.inputs.payload:
|
||||
this.values = this.getContentPayload(config);
|
||||
break;
|
||||
case !!config.inputs.payloadFilePath:
|
||||
this.values = this.getContentPayloadFilePath(config);
|
||||
break;
|
||||
default:
|
||||
config.core.debug(
|
||||
"Missing payload so gathering inputs from action context.",
|
||||
);
|
||||
this.values = github.context;
|
||||
break;
|
||||
}
|
||||
if (config.inputs.payloadDelimiter) {
|
||||
this.values = flatten(this.values, {
|
||||
delimiter: config.inputs.payloadDelimiter,
|
||||
});
|
||||
for (const key of Object.keys(this.values)) {
|
||||
this.values[key] = `${this.values[key]}`;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format request content from payload values for use in the request.
|
||||
* @param {Config} config
|
||||
* @throws if the input payload or payload file path is invalid JSON.
|
||||
* @returns {Content} - the parsed JSON payload to use in requests.
|
||||
*/
|
||||
getContentPayload(config) {
|
||||
if (!config.inputs.payload) {
|
||||
throw new SlackError(
|
||||
config.core,
|
||||
"Invalid input! No payload content was provided",
|
||||
);
|
||||
}
|
||||
try {
|
||||
const input = this.templatize(config, config.inputs.payload);
|
||||
const content = /** @type {Content} */ (
|
||||
yaml.load(input, {
|
||||
schema: yaml.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);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const trimmed = config.inputs.payload.trim();
|
||||
if (
|
||||
!config.inputs.payload.startsWith("{") &&
|
||||
!config.inputs.payload.endsWith("}")
|
||||
) {
|
||||
config.core.debug(
|
||||
"Wrapping input payload in braces to create valid JSON",
|
||||
);
|
||||
const comma = trimmed.replace(/,$/, ""); // remove trailing comma
|
||||
const wrap = `{${comma}}`;
|
||||
return JSON.parse(wrap);
|
||||
}
|
||||
return JSON.parse(trimmed);
|
||||
} catch (/** @type {any} */ error) {
|
||||
throw new SlackError(
|
||||
config.core,
|
||||
"Invalid input! Failed to parse contents of the provided payload",
|
||||
{
|
||||
cause: error,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format request content from the payload file path for use in the request.
|
||||
* @param {Config} config
|
||||
* @throws if the input payload or payload file path is invalid JSON.
|
||||
* @returns {Content} - the parsed JSON payload to use in requests.
|
||||
*/
|
||||
getContentPayloadFilePath(config) {
|
||||
if (!config.inputs.payloadFilePath) {
|
||||
throw new SlackError(
|
||||
config.core,
|
||||
"Invalid input! No payload found for content",
|
||||
);
|
||||
}
|
||||
try {
|
||||
const input = fs.readFileSync(
|
||||
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,
|
||||
});
|
||||
return /** @type {Content} */ (load);
|
||||
}
|
||||
if (config.inputs.payloadFilePath.endsWith("json")) {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
throw new SlackError(
|
||||
config.core,
|
||||
`Invalid input! Failed to parse file extension ${config.inputs.payloadFilePath}`,
|
||||
);
|
||||
} catch (/** @type {any} */ error) {
|
||||
throw new SlackError(
|
||||
config.core,
|
||||
"Invalid input! Failed to parse contents of the provided payload file",
|
||||
{
|
||||
cause: 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.
|
||||
*/
|
||||
templatize(config, input) {
|
||||
if (!config.inputs.payloadTemplated) {
|
||||
return input;
|
||||
}
|
||||
const template = input.replace(/\$\{\{/g, "{{"); // swap ${{ for {{
|
||||
const context = {
|
||||
env: process.env,
|
||||
github: github.context,
|
||||
};
|
||||
return markup.up(template, context);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import core from "@actions/core";
|
||||
|
||||
/**
|
||||
* SlackError is a custom error wrapper for known errors of Slack GitHub Action.
|
||||
*/
|
||||
export default class SlackError extends Error {
|
||||
/**
|
||||
* @typedef Options
|
||||
* @property {Error} [cause] - thrown exception of this error.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {core} _core - GitHub Actions core utilities.
|
||||
* @param {any} error - The error message to throw.
|
||||
* @param {Options} options - configurations of erroring.
|
||||
*/
|
||||
constructor(_core, error, options = {}) {
|
||||
if (error instanceof Error) {
|
||||
super(error.message, { cause: options.cause });
|
||||
} else {
|
||||
super(error, { cause: options.cause });
|
||||
}
|
||||
this.name = "SlackError";
|
||||
if (error.stack) {
|
||||
this.stack = error.stack;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import core from "@actions/core";
|
||||
import send from "./send.js";
|
||||
|
||||
/**
|
||||
* Invoke the Slack GitHub Action job from this file but export actual logic
|
||||
* from the send.js file for testing purposes.
|
||||
*/
|
||||
try {
|
||||
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}`);
|
||||
} else {
|
||||
core.error(`${error}`);
|
||||
}
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import core from "@actions/core";
|
||||
import Client from "./client.js";
|
||||
import Config from "./config.js";
|
||||
import SlackError from "./errors.js";
|
||||
import Webhook from "./webhook.js";
|
||||
|
||||
/**
|
||||
* Orchestrate the action job happenings from inputs to logic to outputs.
|
||||
* @param {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));
|
||||
} catch (/** @type {any} */ error) {
|
||||
config.core.setOutput("time", Math.floor(new Date().valueOf() / 1000));
|
||||
if (config.inputs.errors) {
|
||||
core.setFailed(error);
|
||||
throw new SlackError(core, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the posting action of this workflow with configured settings.
|
||||
* @param {Config} config
|
||||
*/
|
||||
async function post(config) {
|
||||
switch (true) {
|
||||
case !!config.inputs.token:
|
||||
return await new Client().post(config);
|
||||
case !!config.inputs.webhook:
|
||||
return await new Webhook().post(config);
|
||||
default:
|
||||
throw new SlackError(config.core, "No technique given to post content");
|
||||
}
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
import axiosRetry, { exponentialDelay, linearDelay } from "axios-retry";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import Config from "./config.js";
|
||||
import SlackError from "./errors.js";
|
||||
|
||||
/**
|
||||
* This Webhook class posts the configured payload to the provided webhook, with
|
||||
* whatever additional settings set.
|
||||
*/
|
||||
export default class Webhook {
|
||||
/**
|
||||
* @param {Config} config
|
||||
*/
|
||||
async post(config) {
|
||||
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);
|
||||
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,
|
||||
};
|
||||
default:
|
||||
throw new SlackError(
|
||||
config.core,
|
||||
`Unsupported URL protocol: ${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
|
||||
* @returns {import("axios-retry").IAxiosRetryConfig}
|
||||
*/
|
||||
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
|
||||
};
|
||||
case "10":
|
||||
return {
|
||||
retryCondition: axiosRetry.isRetryableError,
|
||||
retries: 10,
|
||||
retryDelay: (count, err) => exponentialDelay(count, err, 2 * 1000), // 34.12 minutes
|
||||
};
|
||||
case "RAPID":
|
||||
return {
|
||||
retryCondition: axiosRetry.isRetryableError,
|
||||
retries: 12,
|
||||
retryDelay: linearDelay(1 * 1000), // 12 seconds
|
||||
};
|
||||
default:
|
||||
return {
|
||||
retryCondition: axiosRetry.isRetryableError,
|
||||
retries: 5,
|
||||
retryDelay: linearDelay(60 * 1000), // 5 minutes
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
import core from "@actions/core";
|
||||
import webapi from "@slack/web-api";
|
||||
import errors from "@slack/web-api/dist/errors.js";
|
||||
import { assert } from "chai";
|
||||
import Client from "../src/client.js";
|
||||
import Config from "../src/config.js";
|
||||
import SlackError from "../src/errors.js";
|
||||
import send from "../src/send.js";
|
||||
import { mocks } from "./index.spec.js";
|
||||
|
||||
describe("client", () => {
|
||||
beforeEach(() => {
|
||||
mocks.reset();
|
||||
});
|
||||
|
||||
describe("inputs", () => {
|
||||
it("requires a method is provided in inputs", async () => {
|
||||
/**
|
||||
* @type {Config}
|
||||
*/
|
||||
const config = {
|
||||
core: core,
|
||||
inputs: {
|
||||
token: "xoxb-example",
|
||||
},
|
||||
};
|
||||
try {
|
||||
await new Client().post(config);
|
||||
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");
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("requires a token is provided in inputs", async () => {
|
||||
/**
|
||||
* @type {Config}
|
||||
*/
|
||||
const config = {
|
||||
core: core,
|
||||
inputs: {
|
||||
method: "chat.postMessage",
|
||||
},
|
||||
};
|
||||
mocks.core.getInput.withArgs("token").returns("xoxb-example-001");
|
||||
try {
|
||||
await new Client().post(config);
|
||||
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");
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("success", () => {
|
||||
it("calls 'chat.postMessage' with the given token and content", async () => {
|
||||
try {
|
||||
const args = {
|
||||
channel: "C0123456789",
|
||||
text: "hello",
|
||||
thread_ts: "1234567890.000001",
|
||||
};
|
||||
const response = {
|
||||
ok: true,
|
||||
channel: "C0123456789",
|
||||
ts: "1234567890.000002",
|
||||
message: {
|
||||
thread_ts: "1234567890.000001",
|
||||
},
|
||||
};
|
||||
mocks.core.getInput.withArgs("method").returns("chat.postMessage");
|
||||
mocks.core.getInput.withArgs("token").returns("xoxb-example");
|
||||
mocks.core.getInput.withArgs("payload").returns(JSON.stringify(args));
|
||||
mocks.api.resolves(response);
|
||||
await send(mocks.core);
|
||||
assert.deepEqual(mocks.api.getCall(0).firstArg, "chat.postMessage");
|
||||
assert.deepEqual(mocks.api.getCall(0).lastArg, args);
|
||||
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");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify(response),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "channel_id");
|
||||
assert.equal(mocks.core.setOutput.getCall(2).lastArg, "C0123456789");
|
||||
assert.equal(mocks.core.setOutput.getCall(3).firstArg, "thread_ts");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(3).lastArg,
|
||||
"1234567890.000001",
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(4).firstArg, "ts");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(4).lastArg,
|
||||
"1234567890.000002",
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(5).firstArg, "time");
|
||||
assert.equal(mocks.core.setOutput.getCalls().length, 6);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
assert.fail("Unexpected error when calling the method");
|
||||
}
|
||||
});
|
||||
|
||||
it("calls 'conversations.create' with the given token and content", async () => {
|
||||
try {
|
||||
const args = {
|
||||
name: "pull-request-review-010101",
|
||||
};
|
||||
const response = {
|
||||
ok: true,
|
||||
channel: {
|
||||
id: "C0101010101",
|
||||
name: "pull-request-review-010101",
|
||||
is_channel: true,
|
||||
created: 1730425428,
|
||||
},
|
||||
};
|
||||
mocks.core.getInput.withArgs("method").returns("chat.postMessage");
|
||||
mocks.core.getInput.withArgs("token").returns("xoxb-example");
|
||||
mocks.core.getInput.withArgs("payload").returns(JSON.stringify(args));
|
||||
mocks.api.resolves(response);
|
||||
await send(mocks.core);
|
||||
assert.deepEqual(mocks.api.getCall(0).firstArg, "chat.postMessage");
|
||||
assert.deepEqual(mocks.api.getCall(0).lastArg, args);
|
||||
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");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify(response),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "channel_id");
|
||||
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);
|
||||
assert.fail("Unexpected error when calling the method");
|
||||
}
|
||||
});
|
||||
|
||||
it("calls 'files.uploadV2' with the provided token and content", async () => {
|
||||
try {
|
||||
const args = {
|
||||
channel: "C0000000001",
|
||||
initial_comment: "the results are in!",
|
||||
file: "results.out",
|
||||
filename: "results-888888.out",
|
||||
};
|
||||
const response = {
|
||||
ok: true,
|
||||
files: [{ id: "F0000000001", created: 1234567890 }],
|
||||
};
|
||||
mocks.core.getInput.withArgs("method").returns("files.uploadV2");
|
||||
mocks.core.getInput.withArgs("token").returns("xoxp-example");
|
||||
mocks.core.getInput.withArgs("payload").returns(JSON.stringify(args));
|
||||
mocks.api.resolves(response);
|
||||
await send(mocks.core);
|
||||
assert.deepEqual(mocks.api.getCall(0).lastArg, args);
|
||||
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");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify(response),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time");
|
||||
assert.equal(mocks.core.setOutput.getCalls().length, 3);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
assert.fail("Unexpected error when calling the method");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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!",
|
||||
},
|
||||
};
|
||||
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.api.rejects(errors.requestErrorWithOriginal(response, true));
|
||||
await send(mocks.core);
|
||||
assert.fail("Expected an error but none was found");
|
||||
} catch (error) {
|
||||
assert.isTrue(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");
|
||||
assert.deepEqual(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify(response),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time");
|
||||
assert.equal(mocks.core.setOutput.getCalls().length, 3);
|
||||
}
|
||||
});
|
||||
|
||||
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: {
|
||||
authorization: "none",
|
||||
},
|
||||
data: {
|
||||
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.api.rejects(errors.httpErrorFromResponse(response));
|
||||
await send(mocks.core);
|
||||
assert.fail("Expected an error but none was found");
|
||||
} catch (error) {
|
||||
assert.isFalse(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");
|
||||
response.body = response.data;
|
||||
response.data = undefined;
|
||||
assert.deepEqual(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify(response),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time");
|
||||
assert.equal(mocks.core.setOutput.getCalls().length, 3);
|
||||
}
|
||||
});
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
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.api.rejects(errors.platformErrorFromResult(response));
|
||||
await send(mocks.core);
|
||||
assert.fail("Expected an error but none was found");
|
||||
} catch (error) {
|
||||
assert.isTrue(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");
|
||||
assert.deepEqual(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify(response),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time");
|
||||
assert.equal(mocks.core.setOutput.getCalls().length, 3);
|
||||
}
|
||||
});
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
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.api.rejects(errors.platformErrorFromResult(response));
|
||||
await send(mocks.core);
|
||||
assert.fail("Expected an error but none was found");
|
||||
} catch (error) {
|
||||
assert.isFalse(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");
|
||||
assert.deepEqual(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify(response),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time");
|
||||
assert.equal(mocks.core.setOutput.getCalls().length, 3);
|
||||
}
|
||||
});
|
||||
|
||||
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.api.rejects(errors.rateLimitedErrorWithDelay(12));
|
||||
await send(mocks.core);
|
||||
assert.fail("Expected an error but none was found");
|
||||
} catch (error) {
|
||||
assert.isFalse(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");
|
||||
assert.deepEqual(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify(response),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time");
|
||||
assert.equal(mocks.core.setOutput.getCalls().length, 3);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
const result = client.retries();
|
||||
assert.equal(
|
||||
result.retries,
|
||||
webapi.retryPolicies.fiveRetriesInFiveMinutes.retries,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not attempt retries when "0" is set', async () => {
|
||||
const webhook = new Client();
|
||||
const result = webhook.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");
|
||||
assert.equal(
|
||||
result.retries,
|
||||
webapi.retryPolicies.fiveRetriesInFiveMinutes.retries,
|
||||
);
|
||||
assert.equal(
|
||||
result.factor,
|
||||
webapi.retryPolicies.fiveRetriesInFiveMinutes.factor,
|
||||
);
|
||||
});
|
||||
|
||||
it('attempts "10" retries in around "30" minutes', async () => {
|
||||
const webhook = new Client();
|
||||
const result = webhook.retries("10");
|
||||
assert.equal(
|
||||
result.retries,
|
||||
webapi.retryPolicies.tenRetriesInAboutThirtyMinutes.retries,
|
||||
);
|
||||
assert.equal(
|
||||
result.factor,
|
||||
webapi.retryPolicies.tenRetriesInAboutThirtyMinutes.factor,
|
||||
);
|
||||
});
|
||||
|
||||
it('attempts a "rapid " burst of "12" retries in seconds', async () => {
|
||||
const webhook = new Client();
|
||||
const result = webhook.retries("rapid ");
|
||||
assert.equal(
|
||||
result.retries,
|
||||
webapi.retryPolicies.rapidRetryPolicy.retries,
|
||||
);
|
||||
assert.equal(result.factor, webapi.retryPolicies.rapidRetryPolicy.factor);
|
||||
});
|
||||
|
||||
it('attempts a "RAPID" burst of "12" retries in seconds', async () => {
|
||||
const webhook = new Client();
|
||||
const result = webhook.retries("RAPID");
|
||||
assert.equal(
|
||||
result.retries,
|
||||
webapi.retryPolicies.rapidRetryPolicy.retries,
|
||||
);
|
||||
assert.equal(result.factor, webapi.retryPolicies.rapidRetryPolicy.factor);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,180 +0,0 @@
|
||||
import { assert } from "chai";
|
||||
import Config from "../src/config.js";
|
||||
import SlackError from "../src/errors.js";
|
||||
import send from "../src/send.js";
|
||||
import { mocks } from "./index.spec.js";
|
||||
|
||||
/**
|
||||
* Confirm values from the action input or environment variables are gathered
|
||||
* or errors are thrown for invalid inputs.
|
||||
*
|
||||
* An assumption is made around these same checks and parsings being done for
|
||||
* each collection of configurations, but only the edge cases of checks are done
|
||||
* here.
|
||||
*/
|
||||
describe("config", () => {
|
||||
beforeEach(() => {
|
||||
mocks.reset();
|
||||
});
|
||||
|
||||
describe("inputs", () => {
|
||||
it("valid values are collected from the action inputs", async () => {
|
||||
mocks.core.getBooleanInput.withArgs("errors").returns(true);
|
||||
mocks.core.getInput.withArgs("method").returns("chat.postMessage");
|
||||
mocks.core.getInput.withArgs("payload").returns('"hello": "world"');
|
||||
mocks.core.getInput.withArgs("proxy").returns("https://example.com");
|
||||
mocks.core.getInput.withArgs("retries").returns("0");
|
||||
mocks.core.getInput.withArgs("token").returns("xoxb-example");
|
||||
const config = new Config(mocks.core);
|
||||
assert.equal(config.inputs.errors, true);
|
||||
assert.equal(config.inputs.method, "chat.postMessage");
|
||||
assert.equal(config.inputs.payload, '"hello": "world"');
|
||||
assert.equal(config.inputs.proxy, "https://example.com");
|
||||
assert.equal(config.inputs.retries, config.Retries.ZERO);
|
||||
assert.equal(config.inputs.token, "xoxb-example");
|
||||
});
|
||||
|
||||
it("errors when both the token and webhook is provided", async () => {
|
||||
mocks.core.getInput.withArgs("token").returns("xoxb-example");
|
||||
mocks.core.getInput.withArgs("webhook").returns("https://example.com");
|
||||
mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook");
|
||||
try {
|
||||
new Config(mocks.core);
|
||||
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.isTrue(mocks.core.setSecret.withArgs("xoxb-example").called);
|
||||
assert.isTrue(
|
||||
mocks.core.setSecret.withArgs("https://example.com").called,
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("errors if neither the token or webhook is provided", async () => {
|
||||
try {
|
||||
new Config(mocks.core);
|
||||
assert.fail("Failed to error when invalid inputs are provided");
|
||||
} catch (err) {
|
||||
if (err instanceof SlackError) {
|
||||
assert.include(
|
||||
err.message,
|
||||
"Missing input! Either a token or webhook is required to take action.",
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("errors if a webhook is provided without the type", async () => {
|
||||
mocks.core.getInput.withArgs("webhook").returns("https://example.com");
|
||||
try {
|
||||
new Config(mocks.core);
|
||||
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'.",
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("errors if the webhook type does not match techniques", async () => {
|
||||
mocks.core.getInput.withArgs("webhook").returns("https://example.com");
|
||||
mocks.core.getInput.withArgs("webhook-type").returns("post");
|
||||
try {
|
||||
new Config(mocks.core);
|
||||
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'.",
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("secrets", async () => {
|
||||
it("treats the provided token as a secret", async () => {
|
||||
mocks.core.getInput.withArgs("token").returns("xoxb-example");
|
||||
try {
|
||||
await send(mocks.core);
|
||||
assert.fail("Failed to error for incomplete inputs while testing");
|
||||
} catch {
|
||||
assert.isTrue(mocks.core.setSecret.withArgs("xoxb-example").called);
|
||||
}
|
||||
});
|
||||
|
||||
it("treats the provided webhook as a secret", async () => {
|
||||
mocks.core.getInput.withArgs("webhook").returns("https://slack.com");
|
||||
mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook");
|
||||
try {
|
||||
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,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("validate", () => {
|
||||
it('allow the "retries" option with lowercased space', async () => {
|
||||
mocks.axios.post.returns(Promise.resolve("LGTM"));
|
||||
mocks.core.getInput.withArgs("retries").returns(" rapid ");
|
||||
mocks.core.getInput
|
||||
.withArgs("webhook")
|
||||
.returns("https://hooks.slack.com");
|
||||
mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook");
|
||||
try {
|
||||
await send(mocks.core);
|
||||
} catch (err) {
|
||||
if (err instanceof SlackError) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Invalid input! An unknown "retries" value was used: FOREVER',
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("errors if an invalid retries option is provided", async () => {
|
||||
mocks.axios.post.returns(Promise.resolve("LGTM"));
|
||||
mocks.core.getInput.withArgs("retries").returns("FOREVER");
|
||||
mocks.core.getInput
|
||||
.withArgs("webhook")
|
||||
.returns("https://hooks.slack.com");
|
||||
mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook");
|
||||
try {
|
||||
await send(mocks.core);
|
||||
} catch (err) {
|
||||
if (err instanceof SlackError) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Invalid input! An unknown "retries" value was used: FOREVER',
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,347 +0,0 @@
|
||||
import path from "node:path";
|
||||
import core from "@actions/core";
|
||||
import { assert } from "chai";
|
||||
import Config from "../src/config.js";
|
||||
import Content from "../src/content.js";
|
||||
import SlackError from "../src/errors.js";
|
||||
import send from "../src/send.js";
|
||||
import { mocks } from "./index.spec.js";
|
||||
|
||||
/**
|
||||
* Confirm values from the action input or environment variables are gathered
|
||||
*/
|
||||
describe("content", () => {
|
||||
beforeEach(() => {
|
||||
mocks.reset();
|
||||
mocks.core.getInput.withArgs("method").returns("chat.postMessage");
|
||||
mocks.core.getInput.withArgs("token").returns("xoxb-example");
|
||||
});
|
||||
|
||||
describe("flatten", () => {
|
||||
it("flattens nested payloads provided with delimiter", async () => {
|
||||
mocks.core.getInput.withArgs("payload").returns(`
|
||||
"apples": "tree",
|
||||
"bananas": {
|
||||
"truthiness": true
|
||||
}
|
||||
`);
|
||||
mocks.core.getInput.withArgs("payload-delimiter").returns("_");
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
apples: "tree",
|
||||
bananas_truthiness: "true",
|
||||
};
|
||||
assert.deepEqual(config.content.values, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("errors if both a payload and file path are provided", async () => {
|
||||
mocks.core.getInput.withArgs("payload").returns(`"message"="hello"`);
|
||||
mocks.core.getInput.withArgs("payload-file-path").returns("example.json");
|
||||
try {
|
||||
await send(mocks.core);
|
||||
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.",
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("payload", async () => {
|
||||
it("parses complete YAML from the input payload", async () => {
|
||||
mocks.core.getInput.withArgs("payload").returns(`
|
||||
message: "this is wrapped"
|
||||
channel: "C0123456789"
|
||||
`);
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
message: "this is wrapped",
|
||||
channel: "C0123456789",
|
||||
};
|
||||
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",
|
||||
"channel": "C0123456789"
|
||||
}
|
||||
`);
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
message: "this is wrapped",
|
||||
channel: "C0123456789",
|
||||
};
|
||||
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.getBooleanInput.withArgs("payload-templated").returns(true);
|
||||
process.env.NUMBER = 12;
|
||||
const config = new Config(mocks.core);
|
||||
process.env.NUMBER = undefined;
|
||||
const expected = {
|
||||
message: "Served 12 from https://api.github.com",
|
||||
};
|
||||
assert.deepEqual(config.content.values, expected);
|
||||
});
|
||||
|
||||
/**
|
||||
* @see {@link https://github.com/slackapi/slack-github-action/issues/203}
|
||||
*/
|
||||
it("templatizes variables with missing variables", async () => {
|
||||
mocks.core.getInput
|
||||
.withArgs("payload")
|
||||
.returns("message: What makes ${{ env.TREASURE }} a secret");
|
||||
mocks.core.getBooleanInput.withArgs("payload-templated").returns(true);
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
message: "What makes ??? a secret",
|
||||
};
|
||||
assert.deepEqual(config.content.values, expected);
|
||||
});
|
||||
|
||||
it("trims last comma JSON with the input payload", async () => {
|
||||
mocks.core.getInput.withArgs("payload").returns(`
|
||||
"message": "LGTM!",
|
||||
"channel": "C0123456789",
|
||||
`);
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
message: "LGTM!",
|
||||
channel: "C0123456789",
|
||||
};
|
||||
assert.deepEqual(config.content.values, expected);
|
||||
});
|
||||
|
||||
it("wraps incomplete JSON from the input payload", async () => {
|
||||
mocks.core.getInput.withArgs("payload").returns(`
|
||||
"message": "LGTM!",
|
||||
"channel": "C0123456789",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"text": "LGTM! :+1:"
|
||||
}
|
||||
}
|
||||
]
|
||||
`);
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
message: "LGTM!",
|
||||
channel: "C0123456789",
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
text: "LGTM! :+1:",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.deepEqual(config.content.values, expected);
|
||||
});
|
||||
|
||||
it("fails if no payload content is provided in input", async () => {
|
||||
/**
|
||||
* @type {Config}
|
||||
*/
|
||||
const config = {
|
||||
core: core,
|
||||
inputs: {
|
||||
payloadFilePath: "unknown.json",
|
||||
},
|
||||
};
|
||||
try {
|
||||
new Content().getContentPayload(config);
|
||||
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",
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("fails if invalid JSON exists in the input payload", async () => {
|
||||
mocks.core.getInput.withArgs("payload").returns("{");
|
||||
try {
|
||||
await send(mocks.core);
|
||||
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",
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("payload file", async () => {
|
||||
it("parses complete YAML from the input payload file", async () => {
|
||||
mocks.core.getInput.withArgs("payload-file-path").returns("example.yaml");
|
||||
mocks.fs.readFileSync
|
||||
.withArgs(path.resolve("example.yaml"), "utf-8")
|
||||
.returns(`
|
||||
message: "drink water"
|
||||
channel: "C6H12O6H2O2"
|
||||
`);
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
message: "drink water",
|
||||
channel: "C6H12O6H2O2",
|
||||
};
|
||||
assert.deepEqual(config.content.values, expected);
|
||||
});
|
||||
|
||||
it("parses complete YML from the input payload file", async () => {
|
||||
mocks.core.getInput.withArgs("payload-file-path").returns("example.yml");
|
||||
mocks.fs.readFileSync
|
||||
.withArgs(path.resolve("example.yml"), "utf-8")
|
||||
.returns(`
|
||||
message: "drink coffee"
|
||||
channel: "C0FFEEEEEEEE"
|
||||
`);
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
message: "drink coffee",
|
||||
channel: "C0FFEEEEEEEE",
|
||||
};
|
||||
assert.deepEqual(config.content.values, expected);
|
||||
});
|
||||
|
||||
it("parses complete JSON from the input payload file", async () => {
|
||||
mocks.core.getInput.withArgs("payload-file-path").returns("example.json");
|
||||
mocks.fs.readFileSync
|
||||
.withArgs(path.resolve("example.json"), "utf-8")
|
||||
.returns(`{
|
||||
"message": "drink water",
|
||||
"channel": "C6H12O6H2O2"
|
||||
}`);
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
message: "drink water",
|
||||
channel: "C6H12O6H2O2",
|
||||
};
|
||||
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 }}"
|
||||
}`);
|
||||
mocks.core.getBooleanInput.withArgs("payload-templated").returns(true);
|
||||
process.env.NUMBER = 12;
|
||||
const config = new Config(mocks.core);
|
||||
process.env.NUMBER = undefined;
|
||||
const expected = {
|
||||
message: "Served 12 from https://api.github.com",
|
||||
};
|
||||
assert.deepEqual(config.content.values, expected);
|
||||
});
|
||||
|
||||
/**
|
||||
* @see {@link https://github.com/slackapi/slack-github-action/issues/203}
|
||||
*/
|
||||
it("templatizes variables with missing variables", async () => {
|
||||
mocks.core.getInput.withArgs("payload-file-path").returns("example.json");
|
||||
mocks.fs.readFileSync
|
||||
.withArgs(path.resolve("example.json"), "utf-8")
|
||||
.returns(`{
|
||||
"message": "What makes $\{\{ env.TREASURE }} a secret"
|
||||
}`);
|
||||
mocks.core.getBooleanInput.withArgs("payload-templated").returns(true);
|
||||
const config = new Config(mocks.core);
|
||||
const expected = {
|
||||
message: "What makes ??? a secret",
|
||||
};
|
||||
assert.deepEqual(config.content.values, expected);
|
||||
});
|
||||
|
||||
it("fails if no payload file is provided in the input", async () => {
|
||||
/**
|
||||
* @type {Config}
|
||||
*/
|
||||
const config = {
|
||||
core: core,
|
||||
inputs: {
|
||||
payload: "LGTM",
|
||||
},
|
||||
};
|
||||
try {
|
||||
new Content().getContentPayloadFilePath(config);
|
||||
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",
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("fails to parse a file path that does not exist", async () => {
|
||||
mocks.core.getInput.withArgs("payload-file-path").returns("unknown.json");
|
||||
try {
|
||||
await send(mocks.core);
|
||||
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",
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("fails to parse a file with an unknown extension", async () => {
|
||||
mocks.core.getInput.withArgs("payload-file-path").returns("unknown.md");
|
||||
try {
|
||||
await send(mocks.core);
|
||||
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.include(
|
||||
err.cause.message,
|
||||
"Invalid input! Failed to parse file extension unknown.md",
|
||||
);
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import core from "@actions/core";
|
||||
import webapi from "@slack/web-api";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import sinon from "sinon";
|
||||
|
||||
/**
|
||||
* Hello experimenter! These tests are here to confirm that the happy paths keep
|
||||
* working and error cases are thrown.
|
||||
*
|
||||
* Tests are grouped with related related code using a file *.spec.js extension.
|
||||
* These test related functionalities of units but a full suite of integration
|
||||
* tests confirm correctness of the actual workflows in .github/workflows/*.yml.
|
||||
*
|
||||
* Actual tests begin in send.spec.js which has integration tests. Other modules
|
||||
* test the edges of this action.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.<string, AxiosError>} 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.
|
||||
*
|
||||
* @see {@link ../action.yml}
|
||||
*/
|
||||
constructor() {
|
||||
this.sandbox = sinon.createSandbox();
|
||||
this.api = sinon.stub(webapi.WebClient.prototype, "apiCall");
|
||||
this.axios = this.sandbox.stub(axios);
|
||||
this.core = this.sandbox.stub(core);
|
||||
this.fs = this.sandbox.stub(fs);
|
||||
this.core.getInput.withArgs("errors").returns("false");
|
||||
this.core.getInput.withArgs("retries").returns("5");
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing interface that removes internal state from existing stubs.
|
||||
*/
|
||||
reset() {
|
||||
this.sandbox.reset();
|
||||
this.api.resetHistory();
|
||||
this.axios.post.resetHistory();
|
||||
this.core.getInput.reset();
|
||||
this.core.getInput.withArgs("errors").returns("false");
|
||||
this.core.getInput.withArgs("retries").returns("5");
|
||||
}
|
||||
}
|
||||
|
||||
export const mocks = new Mock();
|
||||
@@ -1,77 +0,0 @@
|
||||
import { assert } from "chai";
|
||||
import send from "../src/send.js";
|
||||
import { mocks } from "./index.spec.js";
|
||||
|
||||
/**
|
||||
* This is a collection of integration tests that make sure modules are doing
|
||||
* whatever's expected.
|
||||
*
|
||||
* Or at least that's planned...
|
||||
*
|
||||
* Edge cases for inputs are checked in separate modules including the config
|
||||
* specifications.
|
||||
*/
|
||||
describe("send", () => {
|
||||
beforeEach(() => {
|
||||
mocks.reset();
|
||||
});
|
||||
|
||||
describe("techniques", async () => {
|
||||
it("webhook trigger", async () => {
|
||||
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('"greetings": "hello"');
|
||||
mocks.axios.post.returns(
|
||||
Promise.resolve({ status: 200, data: { ok: true } }),
|
||||
);
|
||||
await send(mocks.core);
|
||||
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");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify({ ok: true }),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time");
|
||||
assert.isAtLeast(mocks.core.setOutput.getCall(2).lastArg, 0);
|
||||
});
|
||||
|
||||
it("token", async () => {
|
||||
mocks.api.resolves({ ok: 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"');
|
||||
await send(mocks.core);
|
||||
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");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify({ ok: true }),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time");
|
||||
assert.isAtLeast(mocks.core.setOutput.getCall(2).lastArg, 0);
|
||||
});
|
||||
|
||||
it("incoming webhook", async () => {
|
||||
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('"text": "hello"');
|
||||
mocks.axios.post.returns(Promise.resolve({ status: 200, data: "ok" }));
|
||||
await send(mocks.core);
|
||||
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");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify("ok"),
|
||||
);
|
||||
assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time");
|
||||
assert.isAtLeast(mocks.core.setOutput.getCall(2).lastArg, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,338 +0,0 @@
|
||||
import core from "@actions/core";
|
||||
import { AxiosError } from "axios";
|
||||
import { assert } from "chai";
|
||||
import Config from "../src/config.js";
|
||||
import SlackError from "../src/errors.js";
|
||||
import send from "../src/send.js";
|
||||
import Webhook from "../src/webhook.js";
|
||||
import { mocks } from "./index.spec.js";
|
||||
|
||||
describe("webhook", () => {
|
||||
beforeEach(() => {
|
||||
mocks.reset();
|
||||
});
|
||||
|
||||
describe("success", () => {
|
||||
it("sends the parsed payload to the provided webhook trigger", async () => {
|
||||
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");
|
||||
mocks.axios.post.returns(
|
||||
Promise.resolve({ status: 200, data: { 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.core.setOutput.getCall(0).firstArg, "ok");
|
||||
assert.equal(mocks.core.setOutput.getCall(0).lastArg, true);
|
||||
assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify({ ok: true }),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
assert.fail("Failed to send the webhook");
|
||||
}
|
||||
});
|
||||
|
||||
it("sends the parsed payload to the provided incoming webhook", async () => {
|
||||
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("text: greetings");
|
||||
mocks.axios.post.returns(Promise.resolve({ status: 200, data: "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.core.setOutput.getCall(0).firstArg, "ok");
|
||||
assert.equal(mocks.core.setOutput.getCall(0).lastArg, true);
|
||||
assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response");
|
||||
assert.equal(
|
||||
mocks.core.setOutput.getCall(1).lastArg,
|
||||
JSON.stringify("ok"),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
assert.fail("Failed to send the webhook");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("failure", () => {
|
||||
it("requires that a webhook is provided in inputs", async () => {
|
||||
/**
|
||||
* @type {Config}
|
||||
*/
|
||||
const config = {
|
||||
core: core,
|
||||
inputs: {},
|
||||
};
|
||||
try {
|
||||
await new Webhook().post(config);
|
||||
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");
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("returns the failures from a webhook trigger", async () => {
|
||||
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.axios.post.resolves(Promise.reject(response));
|
||||
try {
|
||||
await send(mocks.core);
|
||||
} catch (err) {
|
||||
if (err instanceof SlackError) {
|
||||
assert.include(err.message, "Request failed with status code 400");
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", 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.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.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.axios.post.resolves(Promise.reject(response));
|
||||
try {
|
||||
await send(mocks.core);
|
||||
} catch (err) {
|
||||
if (err instanceof SlackError) {
|
||||
assert.include(err.message, "Request failed with status code 400");
|
||||
} else {
|
||||
assert.fail("Failed to throw a SlackError", 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.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);
|
||||
});
|
||||
|
||||
it('does not attempt retries when "0" is set', async () => {
|
||||
const webhook = new Webhook();
|
||||
const result = 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!");
|
||||
}
|
||||
assert.equal(
|
||||
result.retryDelay(5, mocks.errors.axios.network_failed),
|
||||
300000,
|
||||
"5th retry after 5 seconds",
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
assert.isAtMost(
|
||||
result.retryDelay(10, mocks.errors.axios.network_failed),
|
||||
3600000,
|
||||
"last attempt is no more than an hour later",
|
||||
);
|
||||
});
|
||||
|
||||
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!");
|
||||
}
|
||||
assert.equal(
|
||||
result.retryDelay(12, mocks.errors.axios.network_failed),
|
||||
12000,
|
||||
"12th retry after 12 seconds",
|
||||
);
|
||||
});
|
||||
|
||||
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!");
|
||||
}
|
||||
assert.equal(
|
||||
result.retryDelay(12, mocks.errors.axios.network_failed),
|
||||
12000,
|
||||
"12th retry after 12 seconds",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user