chore: release

This commit is contained in:
github-actions[bot]
2026-05-01 21:50:05 +00:00
parent 1c0bcf08fe
commit 45a88b9581
80 changed files with 64812 additions and 8337 deletions
-5
View File
@@ -1,5 +0,0 @@
# Changesets
This directory contains [**Changesets**](https://github.com/changesets/changesets) which are markdown files that describe package changes for the next release.
For guidance on when and how to add changesets, checkout the [Maintainer's Guide](../.github/maintainers_guide.md#changesets).
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
-12
View File
@@ -1,12 +0,0 @@
# Salesforce Open Source project configuration
# Learn more: https://github.com/salesforce/oss-template
#ECCN:Open Source
#GUSINFO:Open Source,Open Source Workflow
# @slackapi/slack-github-action
# are code reviewers for all changes in this repo.
* @slackapi/slack-github-action
# @slackapi/developer-education
# are code reviewers for changes in the `/docs` directory.
/docs/ @slackapi/developer-education
-59
View File
@@ -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).
-14
View File
@@ -1,14 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
ignore:
- dependency-name: "@types/node"
versions:
- "25.x"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
-49
View File
@@ -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.
-151
View File
@@ -1,151 +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
Expected behaviors are confirmed with both unit tests and integration tests. Our unit tests run fast without secrets, while integration tests use webhooks and tokens for sending data to Slack across various techniques.
#### Unit tests
Run the following scripts to confirm tests pass before opening a PR:
```sh
$ npm test # Unit tests
$ npm run lint # Lint and format
$ npm run check # Typecheck
```
The `test.yml` workflow runs these scripts for pull requests and changes to the `main` branch.
#### Integration tests
The `integration.yml` workflow uses this action in interactions with Slack using secrets saved to the `staging` environment.
A PR from a forked branch will fail this workflow until a maintainer reviews the code and [dispatches](https://github.com/slackapi/slack-github-action/actions/workflows/integration.yml) a test run that points to the most recent commit using the following format:
```
pull/<NUMBER>/head
```
### Documentation
This repo contains two types of docs files:
- markdown files
- sidebar.json
The private repo containing the docs.slack.dev site pulls these in at build time.
Maintainers need to use the `run workflow` button associated with the `deploy` workflow in that private repo to update the docs with changes from here.
#### Markdown Files
The markdown files here are secretly mdx files in disguise.
If you'd like to add images to pages, add the image files to the same folder the md file is in.
We appreciate markdown edits from anyone!!!
#### Sidebar
`_sidebar.json` sets the slack github action docs sidebar
sidebar values take the form of "slack-github-action/path-within-docs/"
or, in other words - full path but remove "docs":
path: slack-github-action/docs/sending-variables.md
value: slack-github-action/sending-variables
for info on syntax see https://docusaurus.io/docs/sidebar
this file is copied into slackapi.github.io/slack-github-action/sidebar.js it is then called in slackapi.github.io/sidebars.js
### Changesets
This project uses [Changesets](https://github.com/changesets/changesets) to track changes and automate releases.
Each changeset describes a change to the package and its [semver](https://semver.org/) impact, and a new changeset should be added when updating the package with some change that affects consumers:
```sh
npm run changeset
```
Updates to documentation, tests, or CI might not require new entries.
When a PR containing changesets is merged to `main`, a different PR is opened or updated using [changesets/action](https://github.com/changesets/action) which consumes the pending changesets, bumps the package version, and updates the `CHANGELOG` in preparation to release.
### Releases
New versions are published when the release PR created from changesets is merged and the publish workflow is approved. Follow these steps to build confidence:
1. **Check GitHub Milestones**: Before merging the release PR please check the relevant [Milestones](https://github.com/slackapi/slack-github-action/milestones). If issues or pull requests are still open either decide to postpone the release or save those changes for a future update.
2. **Review the release PR**: Verify that the version bump matches expectations, `CHANGELOG` entries are clear, and CI checks pass.
3. **Merge and approve**: Merge the release PR, then approve the publish workflow to release the action. Update the [release](https://github.com/slackapi/slack-github-action/releases) to share to the [marketplace](https://github.com/marketplace/actions/the-slack-github-action).
4. **Update Milestones**: Close the relevant [Milestones](https://github.com/slackapi/slack-github-action/milestones) and rename these to match the released version. Open a new Milestone for the next version.
## Workflow
### 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.
`release/vX.Y.Z` has the packaged distribution of a particular version based from the changes on `main`. This is created using a workflow when new releases are published.
When developing, branches should be created off of your fork and not directly off of this repository. If working on a long-running feature and in collaboration with others, a corresponding branch of the same name is permitted. This makes collaboration on a single branch possible, as contributors working on the same feature cannot push commits to others' open Pull Requests.
After a major version increment, there also may be maintenance branches created specifically for supporting older major versions.
### 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.
-8
View File
@@ -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).
-12
View File
@@ -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"
}
}
-8
View File
@@ -1,8 +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_SERVICE_TOKEN=xoxp-01010101-example
export SLACK_WEBHOOK_TRIGGER=https://hooks.slack.com/triggers/T0123456789/00000000000/abcdefghijklmnopqrstuvwxyz
-6
View File
@@ -1,6 +0,0 @@
# Application values
.env.*
.env
!.env.example
.slack/apps.json
.slack/apps.*.json
-7
View File
@@ -1,7 +0,0 @@
{
"manifest": {
"source": "local"
},
"project_id": "c4805b41-d1ce-4ea0-b297-ed2f8c64c267"
}
-11
View File
@@ -1,11 +0,0 @@
{
"runtime": "actions",
"hooks": {
"deploy": "echo https://api.slack.com/apps",
"get-manifest": "cat ./.slack/manifest.json #"
},
"config": {
"protocol-version": ["default"],
"trigger-paths": ["./.slack/webhook-trigger.json"]
}
}
@@ -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 }}>"
}
]
}
]
}
-71
View File
@@ -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}}"
}
}
}
-149
View File
@@ -1,149 +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 four techniques of interacting with Slack and follows
patterns found in the integration tests.
- **Technique 1** Slack Workflow Builder: Use a Slack webhook trigger to start a
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.
- **Technique 4** Slack CLI Command: Install and run Slack CLI commands such as
`deploy` or `manifest` using a service token.
Configurations for the Slack app and workflow, and the GitHub Actions workflow
are found in the following files:
- Slack app setup: [`.github/resources/.slack/manifest.json`][slacktion]
- GitHub Actions steps: [`.github/workflows/develop.yml`][develop]
> **Note:** During CLI integration tests, `.github/resources/.slack` is moved to
> `.slack` at the project root so the Slack CLI can discover the app manifest.
Either the techniques or app setup and workflow steps can be adjusted during
testing and development. For experimenting with new changes, we recommend using
the [steps for development](#experimenting-for-development) while the
[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
- `SLACK_SERVICE_TOKEN`: xoxp-service-token-example (secret — for CLI commands)
- `SLACK_APP_ID`: A0123456789 (variable — used with `--app` flag in CLI deploy)
### Experimenting for development
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
export SLACK_SERVICE_TOKEN=xoxp-service-token-example
```
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://docs.slack.dev/tools/slack-cli/
[develop]: ../workflows/develop.yml
[manifest]: https://docs.slack.dev/app-manifests/
[secrets]: https://github.com/slackapi/slack-github-action/settings/secrets/actions
[slacktion]: ./.slack/manifest.json
-29
View File
@@ -1,29 +0,0 @@
name: Merge updates to dependencies
on:
pull_request:
jobs:
dependabot:
name: "@dependabot"
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Collect metadata
id: metadata
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Automerge
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-136
View File
@@ -1,136 +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
permissions:
contents: read
steps:
- name: Checkout action
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup the Node runtime for this project
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
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."
-322
View File
@@ -1,322 +0,0 @@
name: Integration tests
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
inputs:
ref:
description: "The branch, tag, or SHA to checkout"
required: true
jobs:
integration:
name: Run API tests
runs-on: ubuntu-latest
environment: staging
permissions:
contents: read
steps:
- name: "check: require maintainer approval"
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::Integration tests from forked branches require maintainer approval."
echo "::notice::Dispatch a test run at ${{ github.server_url }}/${{ github.repository }}/actions/workflows/integration.yml with ref 'pull/${{ github.event.pull_request.number }}/head'"
exit 1
- name: "build: checkout the latest changes"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.sha }}
- name: "build: setup the node runtime"
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
cache: npm
cache-dependency-path: package-lock.json
node-version-file: .nvmrc
- name: "build: install the required dependencies"
run: npm ci
- name: "build: package code for distribution"
run: npm run build
- name: "pretest(inputs): save the push event trigger commit URL"
if: github.event_name == 'push'
id: push
run: |
echo "url=$URL" >> "$GITHUB_OUTPUT"
env:
URL: ${{ github.event.head_commit.url }}
- name: "pretest(inputs): save the pull request event trigger URL"
if: github.event_name == 'pull_request'
id: pull_request
run: |
echo "url=$URL" >> "$GITHUB_OUTPUT"
env:
URL: ${{ github.event.pull_request.html_url }}
- name: "pretest(inputs): save the workflow dispatch event trigger commit URL"
if: github.event_name == 'workflow_dispatch'
id: workflow_dispatch
run: |
echo "url=https://github.com/${GITHUB_REPOSITORY}/commit/${INPUT_REF}" >> "$GITHUB_OUTPUT"
env:
INPUT_REF: ${{ inputs.ref }}
- name: "test(wfb): send a payload to workflow builder via webhook trigger"
id: wfb
uses: ./
with:
errors: true
webhook: ${{ secrets.SLACK_WEBHOOK_TRIGGER }}
webhook-type: webhook-trigger
payload: |
author: ${{ github.event.sender.login }}
channel_id: ${{ secrets.SLACK_CHANNEL_ID }}
event_url: ${{ steps.push.outputs.url || steps.pull_request.outputs.url || steps.workflow_dispatch.outputs.url }}
repo_name: ${{ github.event.repository.full_name }}
status: ${{ job.status }}
- name: "test(wfb): confirm a payload was sent"
run: test -n "$WFB_OUTPUT_TIME"
env:
WFB_OUTPUT_TIME: ${{ steps.wfb.outputs.time }}
- name: "test(api): post a message to channel"
id: message
uses: ./
with:
errors: true
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: ":checkered_flag: Action happens at <https://github.com/${{ github.repository }}>"
- name: "test(api): confirm a message was posted"
run: test -n "$MESSAGE_OUTPUT_TS"
env:
MESSAGE_OUTPUT_TS: ${{ steps.message.outputs.ts }}
- name: "test(api): post a message with blocks"
id: blocks
uses: ./
with:
errors: true
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: ":eyes: Event received..."
attachments:
- color: "dbab09"
fields:
- title: "Status"
short: true
value: "Processing"
- name: "test(api): confirm the blocks were posted"
run: test -n "$BLOCKS_OUTPUT_TS"
env:
BLOCKS_OUTPUT_TS: ${{ steps.blocks.outputs.ts }}
- name: "test(api): post a threaded message"
id: timer
uses: ./
with:
errors: true
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "Started at `${{ steps.blocks.outputs.time }}`"
thread_ts: "${{ steps.blocks.outputs.ts }}"
- name: "test(api): confirm the thread started"
run: test -n "$TIMER_OUTPUT_TIME"
env:
TIMER_OUTPUT_TIME: ${{ steps.timer.outputs.time }}
- name: "test(api): wait to mock event processing"
run: sleep 3
- name: "test(api): update the original message"
id: finished
uses: ./
with:
errors: true
method: chat.update
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
ts: "${{ steps.blocks.outputs.ts }}"
text: ":gear: Event processed!"
attachments:
- color: "28a745"
fields:
- title: "Status"
short: true
value: "Completed"
- name: "test(api): post another threaded message"
id: done
uses: ./
with:
errors: true
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ steps.blocks.outputs.channel_id }}
text: "Finished at `${{ steps.finished.outputs.time }}`"
thread_ts: "${{ steps.timer.outputs.thread_ts }}"
- name: "test(api): post a file into a channel"
id: file
uses: ./
with:
errors: true
method: files.uploadV2
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel_id: ${{ secrets.SLACK_CHANNEL_ID }}
initial_comment: ":robot_face: The codes exists here"
file: .github/workflows/integration.yml
filename: integration.yml
- name: "test(api): react to the completed update message"
uses: ./
with:
errors: true
method: reactions.add
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
timestamp: ${{ steps.blocks.outputs.ts }}
name: "tada"
- name: "test(api): confirm the thread ended"
run: test -n "$DONE_OUTPUT_TIME"
env:
DONE_OUTPUT_TIME: ${{ steps.done.outputs.time }}
- name: "test(incoming): post a message via incoming webhook"
id: incoming
uses: ./
with:
errors: true
webhook: ${{ secrets.SLACK_INCOMING_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
text: "Incoming webhook test for the Slack GitHub Action"
blocks:
- type: section
text:
type: plain_text
text: ":link: A message was received via incoming webhook"
emoji: true
- name: "test(incoming): confirm a webhook was posted"
run: test -n "$INCOMING_WEBHOOK_OUTPUT_TIME"
env:
INCOMING_WEBHOOK_OUTPUT_TIME: ${{ steps.incoming.outputs.time }}
- name: "test(incoming): reveal contents of the github payload"
run: echo "$JSON"
env:
JSON: ${{ toJSON(github) }}
- name: "test(incoming): post a message via payload file"
id: payload_file
uses: ./
with:
errors: true
payload-file-path: ./.github/resources/.slack/incoming-webhook.json
payload-templated: true
webhook: ${{ secrets.SLACK_INCOMING_WEBHOOK }}
webhook-type: incoming-webhook
env:
JOB_STATUS: ${{ job.status }}
ATTACHMENT_COLOR: ${{ (job.status == 'success' && 'good') || (job.status == 'failure' && 'danger') || 'warning' }}
- name: "test(incoming): confirm a payload file was posted"
run: test -n "$PAYLOAD_FILE_OUTPUT_TIME"
env:
PAYLOAD_FILE_OUTPUT_TIME: ${{ steps.payload_file.outputs.time }}
cli:
name: Run CLI tests
runs-on: ${{ matrix.os }}
environment: staging
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
permissions:
contents: read
steps:
- name: "build: checkout the latest changes"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.sha }}
- name: "test(cli): run a slack cli version check"
id: version
uses: ./cli
with:
command: "version"
- name: "test(cli): confirm the version check outputs"
shell: bash
run: |
set -ex
[ "$CLI_OK" = "true" ]
echo "$CLI_TIME" | grep -qE '^[0-9]+$'
[ -n "$CLI_RESPONSE" ]
env:
CLI_OK: ${{ steps.version.outputs.ok }}
CLI_RESPONSE: ${{ steps.version.outputs.response }}
CLI_TIME: ${{ steps.version.outputs.time }}
- name: "test(cli): run an unknown command"
id: unknown
continue-on-error: true
uses: ./cli
with:
command: "off"
- name: "test(cli): confirm the unknown command outputs"
shell: bash
run: |
set -ex
[ "$CLI_OK" = "false" ]
echo "$CLI_TIME" | grep -qE '^[0-9]+$'
[ -n "$CLI_RESPONSE" ]
env:
CLI_OK: ${{ steps.unknown.outputs.ok }}
CLI_RESPONSE: ${{ steps.unknown.outputs.response }}
CLI_TIME: ${{ steps.unknown.outputs.time }}
- name: "chore: configure the actioneering application"
shell: bash
run: mv .github/resources/.slack .slack
- name: "test(cli): validate the app manifest"
uses: ./cli
with:
command: "manifest"
token: ${{ secrets.SLACK_SERVICE_TOKEN }}
- name: "test(cli): deploy the app"
uses: ./cli
with:
command: "deploy --app ${{ vars.SLACK_APP_ID }} --hide-triggers --force"
token: ${{ secrets.SLACK_SERVICE_TOKEN }}
-162
View File
@@ -1,162 +0,0 @@
name: Release
on:
push:
branches:
- main
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
changelog:
name: Changelog
runs-on: ubuntu-latest
outputs:
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
releaseExists: ${{ steps.published.outputs.exists }}
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: true
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
cache: npm
cache-dependency-path: package-lock.json
node-version-file: .nvmrc
- name: Install dependencies
run: npm ci --verbose
- name: Update release PR
id: changesets
uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1.7.0
with:
commit: "chore: release"
title: "chore: release"
version: npm run version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check if publish is needed
id: published
if: steps.changesets.outputs.hasChangesets == 'false'
run: |
VERSION=$(node -p "require('./package.json').version")
if gh release view "v${VERSION}" &>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish:
name: Publish
needs: changelog
if: needs.changelog.outputs.hasChangesets == 'false' && needs.changelog.outputs.releaseExists == 'false'
runs-on: ubuntu-latest
environment: publish
permissions:
contents: write
steps:
- name: Gather credentials
id: credentials
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ secrets.GH_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
permission-contents: write
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: true
token: ${{ steps.credentials.outputs.token }}
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
cache: npm
cache-dependency-path: package-lock.json
node-version-file: .nvmrc
- name: Install dependencies
run: npm ci --verbose
- name: Build a production release
run: npm run build
- name: Gather version
id: version
run: |
VERSION="v$(node -p 'require("./package.json").version')"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "major=$(echo "$VERSION" | cut -d. -f1)" >> "$GITHUB_OUTPUT"
echo "minor=$(echo "$VERSION" | cut -d. -f1-2)" >> "$GITHUB_OUTPUT"
echo "$VERSION"
- name: Check if release exists
id: check
run: |
if gh release view "$VERSION" &>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
env:
VERSION: ${{ steps.version.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release artifacts
if: steps.check.outputs.exists == 'false'
id: release
run: |
awk '/^## /{if(n++)exit}n' CHANGELOG.md | tail -n +3 > RELEASE.md
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "release/$VERSION"
git rm -rf .
git checkout HEAD -- LICENSE
git checkout HEAD -- README.md
git checkout HEAD -- action.yml
git checkout HEAD -- cli/
git add --force dist/
git commit -m "chore: release"
git push origin "release/$VERSION"
SHA=$(git rev-parse HEAD)
git tag -f "$MAJOR" "$SHA"
git tag -f "$MINOR" "$SHA"
git push -f origin "$MAJOR" "$MINOR"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
env:
VERSION: ${{ steps.version.outputs.version }}
MAJOR: ${{ steps.version.outputs.major }}
MINOR: ${{ steps.version.outputs.minor }}
- name: Create GitHub release
if: steps.check.outputs.exists == 'false'
run: gh release create "$VERSION" --target "$SHA" --title "Slack GitHub Action $VERSION" --notes-file RELEASE.md
env:
VERSION: ${{ steps.version.outputs.version }}
SHA: ${{ steps.release.outputs.sha }}
GH_TOKEN: ${{ steps.credentials.outputs.token }}
- name: Notify Slack
if: steps.check.outputs.exists == 'false'
uses: slackapi/slack-github-action@03ea5433c137af7c0495bc0cad1af10403fc800c # v3.0.2
with:
webhook: ${{ secrets.SLACK_RELEASE_ANNOUNCEMENTS_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
action_url: "https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.version }}"
message: "Slack GitHub Action ${{ steps.version.outputs.version }}"
repository: "${{ github.repository }}"
-56
View File
@@ -1,56 +0,0 @@
name: Tests
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
tests:
name: Run unit tests
runs-on: ubuntu-latest
permissions:
checks: write
contents: read
steps:
- name: "build: checkout the latest changes"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: "build: setup the node runtime"
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
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: "unit(test): upload coverage to CodeCov"
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: test/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
- name: "build: package code for distribution"
run: npm run build
- name: "chore(health): check up on recent changes to the health score"
uses: slackapi/slack-health-score@d58a419f15cdaff97e9aa7f09f95772830ab66f7 # v0.1.1
with:
codecov_token: ${{ secrets.CODECOV_API_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
extension: js
include: src
-11
View File
@@ -1,11 +0,0 @@
# Build artifacts
dist
# Development dependencies
node_modules
# Extra files
.DS_Store
# Testing remnants
coverage.txt
-1
View File
@@ -1 +0,0 @@
24
-13
View File
@@ -1,13 +0,0 @@
# slack-github-action
## 3.0.3
### Patch Changes
- 66834e4: feat: add instrumentation to address error rates
## 3.0.2
### Patch Changes
- 79529d7: fix: resolve url.parse deprecation warning for webhook techniques
-39
View File
@@ -1,39 +0,0 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": {
"includes": ["**", "!**/*.json", "!coverage", "!dist"],
"ignoreUnknown": true
},
"formatter": {
"indentStyle": "space"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"vcs": {
"enabled": true,
"clientKind": "git"
}
}
+63286
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1521
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+1
View File
File diff suppressed because one or more lines are too long
-15
View File
@@ -1,15 +0,0 @@
[
{
"type": "doc",
"id": "tools/slack-github-action/slack-github-action",
"label": "Slack GitHub Action",
"className": "sidebar-title"
},
{ "type": "html", "value": "<hr>" },
"tools/slack-github-action/gathering-variables",
"tools/slack-github-action/sending-data-webhook-slack-workflow",
"tools/slack-github-action/sending-data-slack-api-method",
"tools/slack-github-action/sending-data-slack-incoming-webhook",
"tools/slack-github-action/running-slack-cli-commands",
"tools/slack-github-action/additional-configurations"
]
-99
View File
@@ -1,99 +0,0 @@
# Additional configurations
There are some additional, possibly useful, customization options for workflows.
## Exiting with errors
Invalid API requests or unexpected webhook payloads cause a failing response that can be used to fail the GitHub Actions step with the `errors` option.
The `errors` option defaults to `false` so failed requests do not cause the step to fail. This result can still be gathered from the `ok` output.
```yaml
- name: Attempt to call an unknown method
uses: slackapi/[email protected]
with:
errors: true
method: chat.reverse
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
text: "palindrome"
```
Invalid inputs to the GitHub Action, such as not including a payload, will always cause the GitHub step to fail.
## Parsing templated variables
Additional variables provided in the GitHub event [context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6) and event [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads) can be used to replace templated variables in the input payload with the `payload-templated` option:
```yaml
- name: Send custom JSON data to Slack workflow
uses: slackapi/[email protected]
with:
payload-file-path: "./payload-slack-content.json"
payload-templated: true
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
```
This replaces variables templated as `${{ github.payload.repository.html_url }}` with the values found in the GitHub Action event [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads).
## Proxying HTTPS requests
If you need to use a proxy to connect to Slack, you can use the `proxy` option. In this example we use the technique that calls a Slack API method, but configuring a proxy is the same for all techniques:
```yaml
- name: Post to a Slack channel via a proxy
uses: slackapi/[email protected]
with:
method: chat.postMessage
proxy: "http://proxy.example.org:8080" # Change this to a custom value
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "This message was sent through a proxy"
```
The `proxy` option can also be provided with the `HTTPS_PROXY` or `https_proxy` [environment variable](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables) from within the GitHub Actions step.
## Retrying failed requests
Sometimes outgoing requests fail due to [rate limits](/apis/web-api/rate-limits) or similar [HTTP responses](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) and can be retried later.
The `retries` option can be configured to the needs of your workflow with one of these values:
- `0`: No retries, just hope that things go alright.
- `5`: Five retries in five minutes. **Default**.
- `10`: Ten retries in about thirty minutes.
- `RAPID`: A burst of retries to keep things running fast.
```yaml
- name: Attempt a burst of requests
uses: slackapi/[email protected]
with:
method: chat.postMessage
retries: RAPID
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "status: all things are going good"
```
Behind the scenes, [automatic retries](/tools/node-slack-sdk/web-api/#automatic-retries) are handled with the [`@slack/web-api`](/tools/node-slack-sdk/web-api/) package for Slack API methods, and [`axios-retry`](https://www.npmjs.com/package/axios-retry) when sending with a webhook.
## Sending to a custom API URL
In certain circumstances, such as testing the sent payload, a [custom API URL](/tools/node-slack-sdk/web-api/#custom-api-url) can be used to change where `method` requests are sent:
```yaml
- name: Send to a custom API URL
uses: slackapi/[email protected]
with:
api: http://localhost:8080
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "What's happening on localhost?"
```
The default value of `api` is `https://slack.com/api/` for steps using `method`.
-12
View File
@@ -1,12 +0,0 @@
# Gathering variables
There are different techniques to send data into Slack and whichever one is chosen will require a certain set of customized inputs, as described later.
You can provide data to send to Slack from this GitHub Action and either source:
- The default event [context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6) with a [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads) matching the GitHub event.
- A custom payload with optional [variables](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables) provided in the GitHub Action step.
These input options are valid for all techniques, but some techniques require specific constraints with certain requirements for valid inputs.
Additional [configurations](/tools/slack-github-action/additional-configurations) and other details are also available for more customizations to the provided payload.
-128
View File
@@ -1,128 +0,0 @@
# Running Slack CLI commands
The Slack CLI technique installs and runs [Slack CLI](/tools/slack-cli/) commands directly from a GitHub Actions workflow.
This is useful for automating tasks such as deploying apps, validating an app manifest, or interacting with Slack platform features that are available with the CLI.
## Setup
### Authentication
Pass a [service token](/authentication/tokens/) via the `token` input. This is appended as `--token <value>` to the CLI command. The [`slack auth token`](/tools/slack-cli/reference/commands/slack_auth_token) command can be used to gather this.
### CLI version
By default, the latest version of the Slack CLI is installed. To pin a specific version, use the `version` input:
```yaml
- uses: slackapi/slack-github-action/[email protected]
with:
command: "version"
version: "3.14.0"
```
If the `slack` command already exists on `PATH`, installation is skipped entirely.
## Usage
Provide a `command` input with the Slack CLI command to run, omitting the `slack` prefix.
```yaml
- uses: slackapi/slack-github-action/[email protected]
with:
command: "version"
```
## Debug logging
When a workflow is re-run with **Enable debug logging**, the action automatically appends `--verbose` to the CLI command. You can also include `--verbose` in your `command` input manually at any time.
```yaml
- uses: slackapi/slack-github-action/[email protected]
with:
command: "deploy --app ${{ vars.SLACK_APP_ID }} --verbose"
token: ${{ secrets.SLACK_SERVICE_TOKEN }}
```
## Outputs
The following outputs are available after a CLI command runs:
| Output | Type | Description |
| ---------- | --------- | --------------------------------------------------------------------------------------- |
| `ok` | `boolean` | If the command completed with a `0` exit code. |
| `response` | `string` | The standard output from the CLI command. |
| `time` | `number` | The Unix [epoch time](https://en.wikipedia.org/wiki/Unix_time) that the step completed. |
## Examples
### Check the installed CLI version
```yaml
steps:
- uses: slackapi/slack-github-action/[email protected]
id: slack
with:
command: "version"
- run: echo "${{ steps.slack.outputs.response }}"
```
### Validate the app manifest
```yaml
steps:
- uses: actions/checkout@v4
- uses: slackapi/slack-github-action/[email protected]
with:
command: "manifest validate --app ${{ vars.SLACK_APP_ID }}"
token: ${{ secrets.SLACK_SERVICE_TOKEN }}
```
<details>
<summary><strong>Workflow: Validate a manifest</strong></summary>
This workflow validates the app manifest on pull requests to catch configuration issues early.
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_4_Slack_CLI_Command/manifest.yml
```
</details>
### Deploy an app with a service token
```yaml
steps:
- uses: actions/checkout@v4
- uses: slackapi/slack-github-action/[email protected]
with:
command: "deploy --app ${{ vars.SLACK_APP_ID }} --force"
token: ${{ secrets.SLACK_SERVICE_TOKEN }}
```
<details>
<summary><strong>Workflow: Deploy an app</strong></summary>
This workflow deploys a Slack app when changes are pushed to the main branch. It uses a service token to authenticate the deploy command.
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_4_Slack_CLI_Command/deploy.yml
```
</details>
### Manage collaborators
<details>
<summary><strong>Workflow: Manage collaborators</strong></summary>
This workflow adds or removes an app collaborator using a manually triggered workflow.
This example combines the Slack API technique ([`users.lookupByEmail`](https://docs.slack.dev/reference/methods/users.lookupByEmail), [`chat.postMessage`](https://docs.slack.dev/reference/methods/chat.postMessage)) with the CLI technique ([`collaborators add`](https://docs.slack.dev/tools/slack-cli/reference/commands/slack_collaborators_add)/[`remove`](https://docs.slack.dev/tools/slack-cli/reference/commands/slack_collaborators_remove)) to look up a user by email, update collaborators, and post a confirmation message.
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_4_Slack_CLI_Command/collaborators.yml
```
</details>
-181
View File
@@ -1,181 +0,0 @@
# Sending data using a Slack API method
A bot token or user token or [token of some other kind](/authentication/tokens) must be used to call one of [the Slack API methods](/reference/methods) with this technique.
## Setup
Different [Slack API methods](/reference/methods) require different [scopes](/reference/scopes), but setup should be similar for all methods:
1. [Create a Slack app](https://api.slack.com/apps/new) for your workspace or use an existing app.
2. Depending on the Slack API [method](/reference/methods) you wish to call, add the required **scopes** to your app under the **OAuth & Permissions** page on [app settings](https://api.slack.com/apps).
3. Install the app to your workspace using the **Install App** page.
4. Once your app is installed to a workspace, a new [token](/authentication/tokens) with your app's specified scopes will be minted for that workspace. It is worth noting that tokens are only valid for a single workspace! Find the token on the **OAuth & Permissions** page.
5. Add the token as [a repository secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) called `SLACK_BOT_TOKEN` or something similar and memorable.
6. [Add this Action as a step](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idsteps) to your GitHub workflow and provide an input payload to send to the method.
Methods that require an app configuration token should gather this token from the [app configuration token](/app-manifests/configuring-apps-with-app-manifests#config-tokens) settings instead of from a specific app since this token is associated with the workspace.
## Usage
Choosing inputs for these steps is left as an exercise for the actioneer since each of the Slack API methods requires certain values and specific parameters, but these snippets might be helpful when starting.
### Posting a message with text
Posting a message with the [`chat.postMessage`](/reference/methods/chat.postMessage) method can be achieved by adding this step to a job in your GitHub workflow and inviting the bot associated with your app to the channel for posting:
```yaml
- name: Post text to a Slack channel
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "howdy <!channel>!"
```
### Posting a message with blocks
More complex message layouts, such as messages made with [Block Kit](/block-kit/) blocks, can also be sent with one of the Slack API methods:
```yaml
- name: Post blocks to a Slack channel
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
```
### Updating a message
Updating a message after it's posted can be done with the [`chat.update`](/reference/methods/chat.update) method and chaining multiple steps together using outputs from past steps as inputs to current ones:
```yaml
- name: Initiate the deployment launch sequence
id: launch_sequence
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "Deployment started :eyes:"
attachments:
- color: "dbab09"
fields:
- title: "Status"
short: true
value: "In Progress"
- name: Countdown until launch
run: sleep 10
- name: Update the original message with success
uses: slackapi/[email protected]
with:
method: chat.update
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
ts: "${{ steps.launch_sequence.outputs.ts }}"
text: "Deployment finished! :rocket:"
attachments:
- color: "28a745"
fields:
- title: "Status"
short: true
value: "Completed"
```
### Replying to a message
Posting [threaded replies to a message](/messaging/#threading) from a past job can be done by including the `thread_ts` attribute of the parent message in the `payload`:
```yaml
- name: Initiate a deployment
uses: slackapi/[email protected]
id: deployment_message
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "Deployment started :eyes:"
- name: Conclude the deployment
uses: slackapi/[email protected]
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
thread_ts: "${{ steps.deployment_message.outputs.ts }}"
text: "Deployment finished! :rocket:"
```
### Uploading a file
Calling [a Slack API method](/reference/methods) with [`@slack/web-api`](/tools/node-slack-sdk/web-api/) makes [uploading a file](/messaging/working-with-files#uploading_files) just another API call with all of the convenience of the [`files.uploadV2`](/tools/node-slack-sdk/web-api/#upload-a-file) method:
```yaml
- name: Share a file to that channel
uses: slackapi/[email protected]
with:
method: files.uploadV2
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel_id: ${{ secrets.SLACK_CHANNEL_ID }}
initial_comment: "the results are in!"
file: "./path/to/results.out"
filename: "results-${{ github.sha }}.out"
```
## Expected outputs
The technique, like all Slack Github Action techniques, [outputs values](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs) that can be used as inputs in following steps of a GitHub workflow.
The following outputs are returned with each of the techniques:
| Output | Type | Description|
|---|---|---|
|`time` | `number` | The Unix [epoch time](https://en.wikipedia.org/wiki/Unix_time) that the step completed.
| `ok` | `boolean` | If the request completed with success.
| `response` | `string` | The [response](/apis/web-api/#responses) from the request as stringified JSON.
While these outputs are returned with certain Slack API methods:
| Output | Type | Description|
|---|---|---|
|`channel_id` | `string` | The [channel ID](/reference/objects/conversation-object) included in the response.
| `ts`| `string` | The [timestamp](/messaging/retrieving-messages#individual_messages) of the Slack event or message.
| `thread_ts` | `string` | The [timestamp](/messaging/retrieving-messages#individual_messages) of a parent Slack message with [threaded replies](/messaging/retrieving-messages#pulling_threads).
## Example workflows
<details>
<summary><strong>Direct message the author</strong></summary>
This workflow sends a direct message to the user that pushed the most recent commits. It does so by grabbing the email of the pusher.
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_2_Slack_API_Method/author.yml
```
</details>
<details>
<summary><strong>Invite a usergroup to channel</strong></summary>
This workflow creates a channel after a bug is reported and add members of a usergroup by chaining multiple Slack API method calls together.
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_2_Slack_API_Method/invite.yml
```
</details>
@@ -1,96 +0,0 @@
# Sending data as a message with a Slack incoming webhook URL
This technique uses this Action to post a message to a channel or direct message with [incoming webhooks](/messaging/sending-messages-using-incoming-webhooks) and a Slack app.
Incoming webhooks follow the same [formatting](/apis/) patterns as other Slack messaging APIs. Posted messages can be as short as a single line of text, include additional interactivity with [interactive components](/messaging/creating-interactive-messages), or be formatted with [Block Kit](/block-kit/) to build visual components.
## Setup
Gather a Slack incoming webhook URL:
1. [Create a Slack app](https://api.slack.com/apps/new) for your workspace or use an existing app.
2. Add the [`incoming-webhook`](/reference/scopes/incoming-webhook) bot scope under **OAuth & Permissions** page on [app settings](https://api.slack.com/apps).
3. Install the app to your workspace and select a channel to notify from the **Install App** page.
4. Create additional webhooks from the **Incoming Webhooks** page.
5. Add the generated incoming webhook URL as [a repository secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) called `SLACK_WEBHOOK_URL`.
6. [Add this Action as a step](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idsteps) to your GitHub workflow and provide an input payload to send as a message.
The webhook URL will resemble something like so:
```txt
https://hooks.slack.com/services/T0123456789/B1001010101/7IsoQTrixdUtE971O1xQTm4T
```
## Usage
Add the collected webhook from above to a GitHub workflow and configure the step using [`mrkdwn`](/messaging/formatting-message-text) formatting values for a message or [Block Kit](/block-kit/) blocks:
```yaml
- name: Post a message in a channel
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "*GitHub Action build result*: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
```
## Expected outputs
The technique, like all Slack Github Action techniques, [outputs values](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs) that can be used as inputs in following steps of a GitHub workflow.
The following outputs are returned with each of the techniques:
| Output | Type | Description|
|---|---|---|
|`time` | `number` | The Unix [epoch time](https://en.wikipedia.org/wiki/Unix_time) that the step completed.
| `ok` | `boolean` | If the request completed with success.
| `response` | `string` | The [response](/apis/web-api/#responses) from the request as stringified JSON.
## Example workflows
<details>
<summary><strong>Post an inline text message</strong></summary>
This workflow uses incoming webhooks to post a plain text message.
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_3_Slack_Incoming_Webhook/text.yml
```
</details>
<details>
<summary><strong>Post an inline block message</strong></summary>
This workflow uses incoming webhooks to post a message with Block Kit.
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_3_Slack_Incoming_Webhook/blocks.yml
```
</details>
<details>
<summary><strong>Post blocks found in a file</strong></summary>
This workflow uses file data when posting to an incoming webhook. It links to the GitHub Actions job in progress.
Payload file being sent
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_3_Slack_Incoming_Webhook/saved.data.json
```
### Workflow
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_3_Slack_Incoming_Webhook/saved.gha.yml
```
</details>
-192
View File
@@ -1,192 +0,0 @@
# Sending data via a webhook to start a Slack workflow
:::info[This technique requires [a Slack paid plan](https://slack.com/pricing) to use Workflow Builder.]
:::
This technique sends data to Slack using a webhook to start a workflow created using Slack [Workflow Builder](https://slack.com/features/workflow-automation).
## Setup
Start in Slack to create a Slack workflow:
1. [Create a Slack workflow](https://slack.com/help/articles/360041352714-Build-a-workflow--Create-a-workflow-that-starts-outside-of-Slack) that starts from a webhook.
2. Copy the webhook URL and [add it as a repository secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) called `SLACK_WEBHOOK_URL`.
3. [Add this Action as a step](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idsteps) to your GitHub workflow and provide an input payload to send to the webhook.
4. Configure your Slack workflow to use the payload variables sent from the GitHub Action. You can then update the steps of the Slack workflow to use these values in creative and clever ways.
The webhook URL will resemble something like so:
```txt
https://hooks.slack.com/triggers/T0123456789/3141592653589/c6e6c0d868b3054ca0f4611a5dbadaf
```
## Usage
Update the input payloads sent from this GitHub Action to your Slack workflow using the following options:
### Sending values from the default GitHub event context
In the example below, the default GitHub event [context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6) and event [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads) associated with the job that started the GitHub workflow are sent to the provided webhook URL:
```yaml
- name: Send GitHub Action data to a Slack workflow
uses: slackapi/[email protected]
with:
payload-delimiter: "_"
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
```
Accessing variables sent to [Workflow Builder](https://slack.com/features/workflow-automation) with a webhook require that the payload variables are flattened with stringified values. Nested variables in the provided payload can be both flattened and also stringified with the `payload-delimiter` option or changed with other [configurations](/tools/slack-github-action/additional-configurations) to match this format expected from Workflow Builder.
### Providing parsed payload information as strings
Provided input values for payload information are sent to the webhook URL after the job is started:
```yaml
- name: Send custom event details to a Slack workflow
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
status: "${{ job.status }}"
option: "false"
```
### Gathering details of the payload from a saved file
Input values for the payload to be sent can also be provided in a file, either in JSON or YAML format:
```yaml
- name: Send a saved artifact to a Slack workflow
uses: slackapi/[email protected]
with:
payload-file-path: "./artifacts.json"
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
```
### Flattening nested payloads
Variables and data provided in the payload might contain nested fields that need to be flattened before being sent with a [webhook trigger](/tools/slack-github-action/sending-data-webhook-slack-workflow) to match the expected input format of [Workflow Builder](https://slack.com/features/workflow-automation).
The `payload-delimiter` option will flatten the input payload using the provided delimiter and will also make values stringified:
```yaml
- name: Flatten the default GitHub payload
uses: slackapi/[email protected]
with:
payload-delimiter: "_"
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
```
Reference to the flattening implementation is available for exploration from within the [`flat`](https://www.npmjs.com/package/flat) package.
## Expected outputs
The technique, like all Slack Github Action techniques, [outputs values](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs) that can be used as inputs in following steps of a GitHub workflow.
The following outputs are returned with each of the techniques:
| Output | Type | Description|
|---|---|---|
|`time` | `number` | The Unix [epoch time](https://en.wikipedia.org/wiki/Unix_time) that the step completed.
| `ok` | `boolean` | If the request completed with success.
| `response` | `string` | The [response](/apis/web-api/#responses) from the request as stringified JSON.
## Example workflows
<details>
<summary><strong>Format generated files</strong></summary>
This workflow converts build outputs from earlier GitHub Action steps into a Slack message.
This example uses data from a payload file to [send a message](/tools/deno-slack-sdk/reference/slack-functions/send_message/) to a hardcoded channel.
Payload file being sent
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/builds.data.json
```
GitHub Actions workflow
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/builds.gha.yml
```
Slack app manifest
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/builds.manifest.json
```
Slack webhook trigger
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/builds.trigger.json
```
</details>
<details>
<summary><strong>Post release announcements</strong></summary>
This workflow allows you to select a channel to post news about the most recent release to.
This example uses [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions/) and inline inputs to do the following:
1. Open a form to select a channel.
2. Send a message to the selected channel.
3. React with a `:tada:` emoji.
GitHub Actions workflow
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.gha.yml
```
Slack app manifest
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.manifest.json
```
Slack webhook trigger
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.trigger.json
```
</details>
<details>
<summary><strong>Update a channel topic</strong></summary>
This workflow shows the latest commit status in the header of a channel.
This example uses the default GitHub event [context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6) and [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads) to [update a channel topic](/tools/deno-slack-sdk/reference/slack-functions/update_channel_topic/).
GitHub Actions workflow
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/topic.gha.yml
```
Slack app manifest
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/topic.manifest.json
```
Slack webhook trigger
```js reference
https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/topic.trigger.json
```
</details>
-26
View File
@@ -1,26 +0,0 @@
# Slack GitHub Action
The Slack GitHub Action is for sending data to Slack and running commands.
The Slack GitHub Action offers four different techniques for sending data to and interacting with Slack:
* [Send data with a webhook to start a workflow in Workflow Builder](/tools/slack-github-action/sending-data-webhook-slack-workflow).
* [Send data using a Slack API method and a secret token with required scopes](/tools/slack-github-action/sending-data-slack-api-method/).
* [Send data as a message with a Slack incoming webhook URL](/tools/slack-github-action/sending-data-slack-incoming-webhook/).
* [Run Slack CLI commands](/tools/slack-github-action/running-slack-cli-commands).
---
## Versioning
We recommend using the latest version of this GitHub Action for the most recent updates and fixes.
Changes are logged in the [release notes](https://github.com/slackapi/slack-github-action/releases) with migration guides available for updating from `@v1` to the [`@v2`](https://github.com/slackapi/slack-github-action/releases/tag/v2.0.0) and [`@v3`](https://github.com/slackapi/slack-github-action/releases/tag/v3.0.0) releases.
## License
This project is licensed under the [MIT license](https://github.com/slackapi/slack-github-action/blob/main/LICENSE).
## Contributing
All contributions are encouraged! Check out the [contributor's guide](https://github.com/slackapi/slack-github-action/blob/main/.github/contributing.md) to learn more.
@@ -1,80 +0,0 @@
# Technique 1: Slack Workflow Builder
> :memo: This technique uses Workflow Builder, which requires
> [a Slack paid plan][plans].
This technique sends data to Slack using a webhook to start a workflow created
using Slack [Workflow Builder][wfb].
## Setup
The Slack app manifest included with each example showcases steps of a workflow
in [Workflow Builder][wfb] along with the [webhook trigger][triggers] inputs.
For details on how to setup this technique in GitHub Actions, read the [setup][setup] section of the docs.
## Example workflows
1. [**Format generated files**](#format-generated-files): Message outputs from
prior steps.
2. [**Post release announcements**](#post-release-announcements): Share releases
to a channel.
3. [**Update a channel topic**](#update-a-channel-topic): Highlight the current
build status.
### Format generated files
Convert build outputs from earlier GitHub Action steps into a Slack message.
This example uses data from a payload file to [send a message][send_message] to
a hardcoded channel.
**Related files**:
- [`builds.data.json`](./builds.data.json): Payload file being sent.
- [`builds.gha.yml`](./builds.gha.yml): GitHub Actions workflow.
- [`builds.manifest.json`](./builds.manifest.json): Slack app manifest.
- [`builds.trigger.json`](./builds.trigger.json): Slack webhook trigger.
### Post release announcements
Select a channel to post news about the most recent release to.
This example uses [Slack functions][functions] and inline inputs to do the
following:
1. Open a form to select a channel.
2. Send a message to the selected channel.
3. React with a `:tada:` emoji.
**Related files**:
- [`announcements.gha.yml`](./announcements.gha.yml): GitHub Actions workflow.
- [`announcements.manifest.json`](./announcements.manifest.json): Slack app
manifest.
- [`announcements.trigger.json`](./announcements.trigger.json): Slack webhook
trigger.
### Update a channel topic
Show the latest commit status in the header of a channel.
This example uses the default GitHub event [context][event-context] and
[payload][event-payload] to [update a channel topic][update_channel_topic].
**Related files**:
- [`topic.gha.yml`](./topic.gha.yml): GitHub Actions workflow.
- [`topic.manifest.json`](./topic.manifest.json): Slack app manifest.
- [`topic.trigger.json`](./topic.trigger.json): Slack webhook trigger.
[event-context]: https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6
[event-payload]: https://docs.github.com/en/webhooks/webhook-events-and-payloads
[functions]: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-slack-functions/
[giphy]: https://giphy.com
[send_message]: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/send_message
[plans]: https://slack.com/pricing
[setup]: https://docs.slack.dev/tools/slack-github-action/sending-techniques/sending-data-webhook-slack-workflow/
[triggers]: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-webhook-triggers/
[update_channel_topic]: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/update_channel_topic
[wfb]: https://slack.com/features/workflow-automation
@@ -1,22 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Post release announcements
on:
release:
types:
- published
jobs:
run:
name: Share recent changes with a channel
runs-on: ubuntu-latest
steps:
- name: Draft with these release notes details
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
"draft_channel": "${{ secrets.SLACK_CHANNEL_ID }}",
"release_notes": ${{ toJSON(github.event.release.body) }},
"release_repository": "${{ github.repository }}",
"release_version": "${{ github.event.release.tag_name }}",
@@ -1,113 +0,0 @@
{
"_metadata": {
"major_version": 2
},
"display_information": {
"name": "Slack GitHub Actions Releaser",
"description": "Sharing the latest news in channel"
},
"features": {
"app_home": {
"messages_tab_enabled": false
},
"bot_user": {
"display_name": "Slack GitHub Actions Releaser"
}
},
"oauth_config": {
"scopes": {
"bot": ["chat:write", "chat:write.public", "reactions:write"]
}
},
"settings": {
"org_deploy_enabled": true
},
"workflows": {
"release_announcement": {
"title": "Release announcement",
"description": "Share excitement around the latest changes",
"input_parameters": {
"properties": {
"draft_channel": {
"type": "slack#/types/channel_id"
},
"release_notes": {
"type": "string"
},
"release_repository": {
"type": "string"
},
"release_version": {
"type": "string"
}
},
"required": [
"draft_channel",
"release_notes",
"release_repository",
"release_version"
]
},
"steps": [
{
"id": "0",
"function_id": "slack#/functions/send_message",
"inputs": {
"channel_id": "{{inputs.draft_channel}}",
"message": "A new release of <https://github.com/{{inputs.release_repository}}/releases/tag/{{inputs.release_version}}|{{inputs.release_repository}}@{{inputs.release_version}}> was tagged! :rocket:",
"interactive_blocks": [
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Share"
},
"action_id": "share"
}
]
}
]
}
},
{
"id": "1",
"function_id": "slack#/functions/open_form",
"inputs": {
"title": "Share the release notes",
"interactivity": "{{steps.0.interactivity}}",
"submit_label": "Share",
"fields": {
"elements": [
{
"name": "announcement_channel",
"title": "Select an announcements channel",
"type": "slack#/types/channel_id"
}
],
"required": ["announcement_channel"]
}
}
},
{
"id": "2",
"function_id": "slack#/functions/send_message",
"inputs": {
"channel_id": "{{steps.1.fields.announcement_channel}}",
"message": "A new release of <https://github.com/{{inputs.release_repository}}/releases/tag/{{inputs.release_version}}|{{inputs.release_repository}}@{{inputs.release_version}}> was tagged! :rocket:\n```{{inputs.release_notes}}```"
}
},
{
"id": "3",
"function_id": "slack#/functions/add_reaction",
"inputs": {
"message_context": "{{steps.2.message_context}}",
"emoji": "tada"
}
}
]
}
}
}
@@ -1,20 +0,0 @@
{
"type": "webhook",
"name": "Release webhook",
"description": "Share the most recent changes",
"workflow": "#/workflows/release_announcement",
"inputs": {
"draft_channel": {
"value": "{{data.draft_channel}}"
},
"release_notes": {
"value": "{{data.release_notes}}"
},
"release_repository": {
"value": "{{data.release_repository}}"
},
"release_version": {
"value": "{{data.release_version}}"
}
}
}
@@ -1,6 +0,0 @@
{
"status": "done",
"task": "Downstream staging rollout",
"errors": "",
"runner": "github-actions"
}
@@ -1,23 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Format generated files
on:
push:
branches:
- main
jobs:
run:
name: Write structured data as a message
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Start the Slack workflow
uses: slackapi/[email protected]
with:
payload-file-path: "./example-workflows/Technique_1_Slack_Workflow_Builder/builds.data.json"
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
@@ -1,100 +0,0 @@
{
"_metadata": {
"major_version": 2
},
"display_information": {
"name": "Slack GitHub Actions Builder",
"description": "Sharing the latest builds in channel"
},
"features": {
"app_home": {
"messages_tab_enabled": false
},
"bot_user": {
"display_name": "Slack GitHub Actions Builder"
}
},
"oauth_config": {
"scopes": {
"bot": ["chat:write", "chat:write.public"]
}
},
"settings": {
"org_deploy_enabled": true
},
"workflows": {
"build_outputs": {
"title": "Build outputs",
"description": "Write details of the most recent build",
"input_parameters": {
"properties": {
"errors": {
"type": "string"
},
"runner": {
"type": "string"
},
"status": {
"type": "string"
},
"task": {
"type": "string"
}
},
"required": ["errors", "runner", "status", "task"]
},
"steps": [
{
"id": "0",
"function_id": "slack#/functions/send_message",
"inputs": {
"channel_id": "C0123456789",
"message": [
{
"type": "rich_text",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "{{inputs.task}}: ",
"style": {
"bold": true
}
},
{
"type": "text",
"text": "{{inputs.status}}",
"style": {
"code": true
}
}
]
},
{
"type": "rich_text_quote",
"elements": [
{
"type": "text",
"text": "{{inputs.errors}} \n"
},
{
"type": "emoji",
"name": "robot_face"
},
{
"type": "text",
"text": " {{inputs.runner}}"
}
]
}
]
}
]
}
}
]
}
}
}
@@ -1,20 +0,0 @@
{
"type": "webhook",
"name": "Status sharing",
"description": "Update a channel with build results",
"workflow": "#/workflows/build_outputs",
"inputs": {
"errors": {
"value": "{{data.errors}}"
},
"runner": {
"value": "{{data.runner}}"
},
"status": {
"value": "{{data.status}}"
},
"task": {
"value": "{{data.task}}"
}
}
}
@@ -1,18 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Update a channel topic
on:
push:
branches:
- main
jobs:
run:
name: Keep track of the main branch
runs-on: ubuntu-latest
steps:
- name: Update the channel topic
uses: slackapi/[email protected]
with:
payload-delimiter: "_"
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
@@ -1,52 +0,0 @@
{
"_metadata": {
"major_version": 2
},
"display_information": {
"name": "Slack GitHub Actions Commits",
"description": "Reading the latest pushes"
},
"features": {
"app_home": {
"messages_tab_enabled": false
},
"bot_user": {
"display_name": "Slack GitHub Actions Commits"
}
},
"oauth_config": {
"scopes": {
"bot": ["channels:manage", "groups:write.topic"]
}
},
"settings": {
"org_deploy_enabled": true
},
"workflows": {
"follow_recent_commits": {
"title": "Follow recent commits",
"description": "Write details of the most recent build",
"input_parameters": {
"properties": {
"commit_message": {
"type": "string"
},
"commit_repository": {
"type": "string"
}
},
"required": ["commit_message", "commit_repository"]
},
"steps": [
{
"id": "0",
"function_id": "slack#/functions/update_channel_topic",
"inputs": {
"channel_id": "C0123456789",
"topic": ":large_green_square: *{{inputs.commit_repository}}* {{inputs.commit_message}}"
}
}
]
}
}
}
@@ -1,14 +0,0 @@
{
"type": "webhook",
"name": "Status sharing",
"description": "Update a channel with commit details",
"workflow": "#/workflows/follow_recent_commits",
"inputs": {
"commit_message": {
"value": "{{data.payload_head_commit_message}}"
},
"commit_repository": {
"value": "{{data.payload_repository_full_name}}"
}
}
}
@@ -1,39 +0,0 @@
# Technique 2: Slack API method
A [token][tokens], such as a bot or user token, must be used to call one of
[the Slack API methods][methods] with this technique.
## Setup
For details on how to setup this technique in GitHub Actions, read the [setup][setup] section of the docs.
## Example workflows
1. [**Direct message the author**](#direct-message-the-author): Write to the
Slack user with a matching email.
2. [**Invite a usergroup to channel**](#invite-a-usergroup-to-channel): Create a
channel and invite members.
### Direct message the author
Send a direct message to the user that pushed the most recent commits.
This example uses the email of the pusher to find the user to send a message to.
**Related files**:
- [`author.yml`](./author.yml): GitHub Actions workflow.
### Invite a usergroup to channel
Create a channel after a bug is reported and add members of a usergroup.
This example chains multiple Slack API methods together to help fix bugs fast.
**Related files**:
- [`invite.yml`](./invite.yml): GitHub Actions workflow.
[methods]: https://docs.slack.dev/reference/methods/
[setup]: https://docs.slack.dev/tools/slack-github-action/sending-techniques/sending-data-slack-api-method/
[tokens]: https://docs.slack.dev/authentication/tokens/
@@ -1,56 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Direct message the author
on:
push:
jobs:
run:
name: Send a notification of recent changes
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Find correspondences
id: email
uses: slackapi/[email protected]
with:
method: users.lookupByEmail # https://docs.slack.dev/reference/methods/users.lookupByEmail/
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
email: ${{ github.event.pusher.email }}
- name: Search email detail
if: ${{ steps.email.outputs.ok }}
run: |
SLACK_USER_ID=$(echo '${{ steps.email.outputs.response }}' | jq -r '.user.id')
echo "SLACK_USER_ID=$SLACK_USER_ID" >> $GITHUB_ENV
- name: Send a direct message
if: ${{ steps.email.outputs.ok }}
uses: slackapi/[email protected]
with:
errors: true
method: chat.postMessage # https://docs.slack.dev/reference/methods/chat.postMessage/
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
"channel": "${{ env.SLACK_USER_ID }}",
"text": "${{ github.repository }} had a change!",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":large_green_square: <https://github.com/${{ github.repository }}|*${{ github.repository }}*>: `${{ github.sha }}`"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ${{ toJSON(github.event.head_commit.message) }}
}
}
]
@@ -1,57 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Invite a usergroup to channel
on:
issues:
types:
- labeled
jobs:
run:
name: Respond to reports of a new problem
runs-on: ubuntu-latest
if: ${{ github.event.label.name == 'bug' }}
steps:
- name: Create a new Slack channel for communications
id: conversation
uses: slackapi/[email protected]
with:
errors: true
method: conversations.create # https://docs.slack.dev/reference/methods/conversations.create/
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
name: issue-${{ github.event.issue.number }}
- name: Send the issue link into the Slack channel
uses: slackapi/[email protected]
with:
errors: true
method: chat.postMessage # https://docs.slack.dev/reference/methods/chat.postMessage/
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ steps.conversation.outputs.channel_id }}
text: "An issue was opened <!date^${{ steps.conversation.outputs.time }}^{date_num} at {time_secs}|just now>: ${{ github.event.issue.html_url }}"
- name: Gather information of those to add
id: members
uses: slackapi/[email protected]
with:
errors: true
method: usergroups.users.list # https://docs.slack.dev/reference/methods/usergroups.users.list/
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
usergroup: ${{ secrets.SLACK_USERGROUP_ID }}
- name: Combine the list of usergroup users
run: |
SLACK_USERGROUP_USER_IDS=$(echo '${{ steps.members.outputs.response }}' | jq -r '.users | join(",")' )
echo "SLACK_USERGROUP_USER_IDS=$SLACK_USERGROUP_USER_IDS" >> $GITHUB_ENV
- name: Add the usergroup to the channel
uses: slackapi/[email protected]
with:
errors: true
method: conversations.invite # https://docs.slack.dev/reference/methods/conversations.invite/
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ steps.conversation.outputs.channel_id }}
users: ${{ env.SLACK_USERGROUP_USER_IDS }}
@@ -1,56 +0,0 @@
# Technique 3: Slack incoming webhook
This technique uses this Action to post a message to a channel or direct message
with [incoming webhooks][incoming-webhook] and a Slack app.
Incoming webhooks follow the same [formatting][formatting] patterns as other
Slack messaging APIs. Posted messages can be as short as a single line of text,
include additional interactivity with [interactive components][interactivity],
or be formatted with [Block Kit][block-kit] to build visual components.
## Setup
For details on how to setup this technique in GitHub Actions, read the [setup][setup] section of the docs.
## Example workflows
1. [Post an inline text message](#post-an-inline-text-message)
2. [Post an inline block message](#post-an-inline-block-message)
3. [Post blocks found in a file](#post-blocks-found-in-a-file)
### Post an inline text message
Write a line of text after a push event is received.
This example uses incoming webhooks to post a plain text message.
**Related files**:
- [`text.yml`](./text.yml): GitHub Actions workflow.
### Post an inline block message
Format a response to recent adventures.
This example uses incoming webhooks to post a message with Block Kit.
**Related files**:
- [`blocks.yml`](./blocks.yml): GitHub Actions workflow.
### Post blocks found in a file
Link to the GitHub Actions job in progress.
This example uses file data when posting to an incoming webhook.
**Related files**:
- [`saved.data.json`](./saved.data.json): Payload file being sent.
- [`saved.gha.yml`](./saved.gha.yml): GitHub Actions workflow.
[block-kit]: https://docs.slack.dev/messaging/#complex_layouts
[formatting]: https://docs.slack.dev/messaging/formatting-message-text/
[incoming-webhook]: https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/
[interactivity]: https://docs.slack.dev/messaging/creating-interactive-messages/
[setup]: https://docs.slack.dev/tools/slack-github-action/sending-techniques/sending-data-slack-incoming-webhook/
@@ -1,36 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Post an inline block message
on:
push:
branches:
- main
jobs:
run:
name: Share a travel review to channel
runs-on: ubuntu-latest
steps:
- name: Write the review
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
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"
text:
type: "mrkdwn"
text: "<https://example.com|*Overlook Hotel*> :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"
fields:
- type: "mrkdwn"
text: "*Average Rating*: 1.0"
@@ -1,34 +0,0 @@
{
"channel": "${{ env.SLACK_CHANNEL_ID }}",
"text": "Messages met made meetings meet",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Slack notifications sound following the most recent commit pushed `${{ github.sha }}`"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "Inspect",
"emoji": true
},
"value": "actions",
"url": "https://github.com/${{ github.payload.repository.full_name }}/actions/runs/${{ github.runId }}",
"action_id": "run"
}
},
{
"type": "image",
"title": {
"type": "plain_text",
"text": "@slackbot has the answers",
"emoji": true
},
"image_url": "https://media.makeameme.org/created/a-slack-this.jpg",
"alt_text": "Baby Yoda professes the power of a Slack meeting"
}
]
}
@@ -1,26 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Post blocks found in a file
on:
push:
branches:
- main
jobs:
run:
name: Share a travel review to channel
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Forward a saved message
uses: slackapi/[email protected]
with:
payload-file-path: "./example-workflows/Technique_3_Slack_Incoming_Webhook/saved.data.json"
payload-templated: true
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
@@ -1,19 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Post an inline text message
on:
push:
branches:
- main
jobs:
run:
name: Leave a kind message after updates
runs-on: ubuntu-latest
steps:
- name: Greet the reader
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Greetings!"
@@ -1,37 +0,0 @@
# Technique 4: Slack CLI Command
A [service token](https://docs.slack.dev/authentication/tokens/#service) is used to install and run [Slack CLI](https://docs.slack.dev/tools/slack-cli/) commands directly from a GitHub Actions workflow with this technique.
## Setup
For details on how to setup this technique in GitHub Actions, read the [setup](https://docs.slack.dev/tools/slack-github-action/sending-techniques/running-slack-cli-commands/) section of the docs.
## Example workflows
1. [**Deploy an app**](#deploy-an-app): Deploy to Slack on push to the main branch.
2. [**Validate the app manifest**](#validate-the-app-manifest): Check the app manifest on pull requests.
3. [**Manage a collaborator**](#manage-a-collaborator): Add or remove an app collaborator using CLI and API techniques together.
### Deploy an app
Deploy a Slack app when changes are pushed to the main branch. This example uses a service token to authenticate the deploy command.
**Related files**:
- [`deploy.yml`](./deploy.yml): GitHub Actions workflow.
### Validate the app manifest
Run manifest validation on pull requests to catch configuration issues early. This example checks the app manifest file in the repository.
**Related files**:
- [`manifest.yml`](./manifest.yml): GitHub Actions workflow.
### Manage a collaborator
Add or remove an app collaborator using a manually triggered workflow with [`workflow_dispatch`](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow). This example combines the Slack API technique ([`users.lookupByEmail`](https://docs.slack.dev/reference/methods/users.lookupByEmail), [`chat.postMessage`](https://docs.slack.dev/reference/methods/chat.postMessage)) with the CLI technique ([`collaborators add`](https://docs.slack.dev/tools/slack-cli/reference/commands/slack_collaborators_add)/[`remove`](https://docs.slack.dev/tools/slack-cli/reference/commands/slack_collaborators_remove)) to look up a user by email, update collaborators, and post a confirmation message.
**Related files**:
- [`collaborators.yml`](./collaborators.yml): GitHub Actions workflow.
@@ -1,59 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Manage a collaborator
on:
workflow_dispatch:
inputs:
email:
description: "The collaborator's email address"
required: true
type: string
add:
description: "Checked to add, unchecked to remove"
required: false
default: true
type: boolean
jobs:
collaborator:
name: Add or remove a collaborator
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Look up the Slack user by email
id: email
uses: slackapi/[email protected]
with:
method: users.lookupByEmail # https://docs.slack.dev/reference/methods/users.lookupByEmail/
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
email: ${{ inputs.email }}
- name: Gather the display name
if: ${{ steps.email.outputs.ok }}
run: |
SLACK_USER_ID=$(echo '${{ steps.email.outputs.response }}' | jq -r '.user.id')
SLACK_DISPLAY_NAME=$(echo '${{ steps.email.outputs.response }}' | jq -r '(.user.profile.display_name | select(. != "")) // .user.real_name')
echo "SLACK_USER_ID=$SLACK_USER_ID" >> "$GITHUB_ENV"
echo "SLACK_DISPLAY_NAME=$SLACK_DISPLAY_NAME" >> "$GITHUB_ENV"
- name: Add or remove the collaborator
if: ${{ steps.email.outputs.ok }}
uses: slackapi/slack-github-action/[email protected]
with:
command: "collaborators ${{ inputs.add && 'add' || 'remove' }} ${{ inputs.email }} --app ${{ vars.SLACK_APP_ID }}"
token: ${{ secrets.SLACK_SERVICE_TOKEN }}
- name: Post a confirmation message
if: ${{ steps.email.outputs.ok }}
uses: slackapi/[email protected]
with:
errors: true
method: chat.postMessage # https://docs.slack.dev/reference/methods/chat.postMessage/
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "<@${{ env.SLACK_USER_ID }}> (${{ env.SLACK_DISPLAY_NAME }}) was ${{ inputs.add && 'added to' || 'removed from' }} the app collaborators."
@@ -1,32 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Deploy an app
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy to Slack
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Deploy the app
uses: slackapi/slack-github-action/[email protected]
with:
command: "deploy --app ${{ vars.SLACK_APP_ID }} --force"
token: ${{ secrets.SLACK_SERVICE_TOKEN }}
- name: Post a deployment message
uses: slackapi/[email protected]
with:
errors: true
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "Deployed to Slack from ${{ github.sha }}"
@@ -1,20 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Validate the app manifest
on:
pull_request:
jobs:
validate:
name: Check the app manifest
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Validate the manifest
uses: slackapi/slack-github-action/[email protected]
with:
command: "manifest validate --app ${{ vars.SLACK_APP_ID }}"
token: ${{ secrets.SLACK_SERVICE_TOKEN }}
-28
View File
@@ -1,28 +0,0 @@
{
"compilerOptions": {
"strict": true,
"checkJs": true,
"types": ["node"],
"esModuleInterop": true,
"lib": [
"ESNext"
],
"module": "nodenext",
"moduleResolution": "nodenext",
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"target": "ESNext"
},
"exclude": [
"node_modules"
],
"include": [
"src/**/*.js"
],
"jsdoc": {
"out": "support/jsdoc",
"access": "public"
}
}
-2370
View File
File diff suppressed because it is too large Load Diff
-68
View File
@@ -1,68 +0,0 @@
{
"name": "@slack/slack-github-action",
"version": "3.0.3",
"private": true,
"description": "The official Slack GitHub Action. Use this to send data into your Slack workspace",
"main": "dist/index.js",
"files": [
"action.yml",
"cli/",
"dist/"
],
"type": "module",
"scripts": {
"build": "ncc build src/index.js --license licenses.txt --source-map",
"changeset": "npx @changesets/cli",
"check": "tsc --noemit --project ./jsconfig.json",
"dev": "act public --eventpath .github/resources/.actions/event.json --secret-file .github/resources/.env --platform ubuntu-latest=node:24 --container-architecture linux/amd64",
"docs": "TAG=\"v$(npm pkg get version | jq -r)\" && grep -rl 'slackapi/slack-github-action@v' ./docs ./example-workflows | xargs sed -i \"s|slackapi/slack-github-action@v.*|slackapi/slack-github-action@${TAG}|g\" && grep -rl 'slackapi/slack-github-action/cli@v' ./docs ./example-workflows | xargs sed -i \"s|slackapi/slack-github-action/cli@v.*|slackapi/slack-github-action/cli@${TAG}|g\"",
"lint": "biome check",
"lint:fix": "biome check --write",
"test": "node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=test/coverage.txt test/*.spec.js",
"version": "npm run changeset version && npm install && npm run docs"
},
"repository": {
"type": "git",
"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": ">=24.11.0",
"npm": ">=11.6.1"
},
"homepage": "https://docs.slack.dev/tools/slack-github-action/",
"dependencies": {
"@actions/core": "^3.0.1",
"@actions/github": "^9.1.1",
"@slack/logger": "^4.0.1",
"@slack/web-api": "^7.15.1",
"axios": "^1.15.0",
"axios-retry": "^4.5.0",
"flat": "^6.0.1",
"https-proxy-agent": "^8.0.0",
"js-yaml": "^4.1.1",
"markup-js": "^1.5.21"
},
"devDependencies": {
"@biomejs/biome": "^2.4.13",
"@changesets/cli": "^2.31.0",
"@types/flat": "^5.0.5",
"@types/js-yaml": "^4.0.9",
"@types/markup-js": "^1.5.0",
"@types/node": "^24.12.2",
"@types/sinon": "^21.0.0",
"@vercel/ncc": "^0.38.4",
"sinon": "^21.0.3",
"typescript": "^6.0.2"
}
}
-116
View File
@@ -1,116 +0,0 @@
import webapi 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://docs.slack.dev/tools/node-slack-sdk/web-api/}
* @see {@link https://docs.slack.dev/reference/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,
allowAbsoluteUrls: false,
logger: config.logger,
retryConfig: this.retries(config.inputs.retries),
slackApiUrl: config.inputs.api || undefined,
});
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;
}
}
}
-216
View File
@@ -1,216 +0,0 @@
import os from "node:os";
import webapi from "@slack/web-api";
import axios from "axios";
import packageJson from "../package.json" with { type: "json" };
import Content from "./content.js";
import SlackError from "./errors.js";
import Logger from "./logger.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 {string?} api - A custom API URL to send method requests to.
* @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 - Separators of nested attributes.
* @property {string?} payloadFilePath - Location of a JSON request payload.
* @property {boolean} payloadTemplated - If templated values are replaced.
* @property {string?} proxy - An optional proxied connection for requests.
* @property {Retries} retries - The retries method to use for failed requests.
* @property {string?} 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;
/**
* The logger of outputs.
* @type {import("@slack/logger").Logger}
*/
logger;
/**
* @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 {import("@actions/core")} core - GitHub Actions core utilities.
*/
constructor(core) {
this.axios = axios;
this.core = core;
this.logger = new Logger(core).logger;
this.webapi = webapi;
this.inputs = {
api: core.getInput("api"),
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.instrument();
this.mask();
this.validate(core);
core.debug(`Gathered action inputs: ${JSON.stringify(this.inputs)}`);
this.content = new Content().get(this);
core.debug(`Parsed request content: ${JSON.stringify(this.content)}`);
}
/**
* Add user agent metadata for instrumentation.
*/
instrument() {
this.webapi.addAppMetadata({
name: packageJson.name,
version: packageJson.version,
});
this.axios.defaults.headers.common["User-Agent"] =
`${packageJson.name.replace("/", ":")}/${packageJson.version} ` +
`axios/${this.axios.VERSION} ` +
`node/${process.version.replace("v", "")} ` +
`${os.platform()}/${os.release()}`;
}
/**
* Hide secret values provided in the inputs from appearing.
*/
mask() {
if (this.inputs.token) {
this.core.debug("Setting the provided token as a secret variable.");
this.core.setSecret(this.inputs.token);
}
if (this.inputs.webhook) {
this.core.debug("Setting the provided webhook as a secret variable.");
this.core.setSecret(this.inputs.webhook);
}
}
/**
* Confirm the configurations are correct enough to continue.
* @param {import("@actions/core")} core - GitHub Actions core utilities.
*/
validate(core) {
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 !!core.getInput("token") && !!core.getInput("webhook"):
throw new SlackError(
core,
"Invalid input! Either the token or webhook is required - not both.",
);
case !!this.inputs.method:
if (!this.inputs.token) {
throw new SlackError(
core,
"Missing input! A token must be provided to use the method decided.",
);
}
break;
case !!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 method or webhook is required to take action.",
);
}
}
}
-180
View File
@@ -1,180 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import * as 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.payloadTemplated) {
this.values = this.templatize(this.values);
}
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) {
const errors = [];
if (!config.inputs.payload) {
throw new SlackError(
config.core,
"Invalid input! No payload content was provided",
);
}
try {
const content = /** @type {Content} */ (
yaml.load(config.inputs.payload, {
schema: yaml.JSON_SCHEMA,
})
);
return /** @type {Content} */ (content);
} catch (error) {
if (error instanceof Error) {
errors.push(error);
}
}
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) {
errors.unshift(error);
throw new SlackError(
config.core,
"Invalid input! Failed to parse contents of the provided payload",
{
cause: { values: errors },
},
);
}
}
/**
* 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",
);
if (
config.inputs.payloadFilePath.endsWith("yaml") ||
config.inputs.payloadFilePath.endsWith("yml")
) {
const load = yaml.load(input, {
schema: yaml.JSON_SCHEMA,
});
return /** @type {Content} */ (load);
}
if (config.inputs.payloadFilePath.endsWith("json")) {
return JSON.parse(input);
}
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: { values: [error] },
},
);
}
}
/**
* Replace templated variables in the provided content as requested.
* @param {unknown} input - The initial value of the content.
* @returns {unknown} Content with templatized variables replaced.
*/
templatize(input) {
if (Array.isArray(input)) {
return input.map((v) => this.templatize(v));
}
if (input && typeof input === "object") {
/**
* @type {Record<string, unknown>}
*/
const out = {};
for (const [k, v] of Object.entries(input)) {
out[k] = this.templatize(v);
}
return out;
}
if (typeof input === "string") {
const template = input.replace(/\$\{\{/g, "{{"); // swap ${{ for {{
const context = {
env: process.env,
github: github.context,
};
return markup.up(template, context);
}
return input;
}
}
-31
View File
@@ -1,31 +0,0 @@
/**
* @typedef Cause
* @property {Error[]} [values] - Caught exceptions.
*/
/**
* SlackError is a custom error wrapper for known errors of Slack GitHub Action.
*/
export default class SlackError extends Error {
/**
* @typedef Options
* @property {Cause} [cause] - Reason for an error.
*/
/**
* @param {import("@actions/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;
}
}
}
-26
View File
@@ -1,26 +0,0 @@
import * as 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 {
await send(core);
} catch (error) {
if (error instanceof Error) {
core.error(error.message);
/** @type {import('./errors.js').Cause} */
const causes = /** @type {any} */ (error.cause);
if (causes?.values) {
for (const cause of causes.values) {
core.info(`${cause.stack}`);
}
} else {
core.info(`${error.stack}`);
}
} else {
core.error(`${error}`);
}
throw error;
}
-32
View File
@@ -1,32 +0,0 @@
import { LogLevel } from "@slack/logger";
/**
* The Logger class creates a Logger to output debug messages and errors.
*
* @see {@link https://docs.slack.dev/tools/node-slack-sdk/web-api/#logging}
*/
export default class Logger {
/**
* The logger for outputs.
* @type {import("@slack/logger").Logger}
*/
logger;
/**
* Shared utilities specific to the GitHub action workflow.
* @param {import("@actions/core")} core - GitHub Actions core utilities.
*/
constructor(core) {
this.logger = {
debug: core.debug,
info: core.info,
warn: core.warning,
error: core.error,
getLevel: () => {
return core.isDebug() ? LogLevel.DEBUG : LogLevel.INFO;
},
setLevel: (_level) => {},
setName: (_name) => {},
};
}
}
-38
View File
@@ -1,38 +0,0 @@
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 {import("@actions/core")} core - GitHub Actions core utilities.
* @throws if an error happens but might not cause the job to fail.
*/
export default async function send(core) {
const config = new Config(core);
try {
await post(config);
config.core.setOutput("time", Math.floor(Date.now() / 1000));
} catch (/** @type {any} */ error) {
config.core.setOutput("time", Math.floor(Date.now() / 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.method:
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
View File
@@ -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
};
}
}
}
-546
View File
@@ -1,546 +0,0 @@
import assert from "node:assert";
import { beforeEach, describe, it } from "node:test";
import webapi from "@slack/web-api";
import errors from "@slack/web-api/dist/errors.js";
import sinon from "sinon";
import Client from "../src/client.js";
import Config from "../src/config.js";
import SlackError from "../src/errors.js";
import Logger from "../src/logger.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: mocks.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.ok(err.message.includes("No API method was provided for use"));
} else {
assert.fail(err);
}
}
});
it("requires a token is provided in inputs", async () => {
/**
* @type {Config}
*/
const config = {
core: mocks.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.ok(err.message.includes("No token was provided to post with"));
} else {
assert.fail(err);
}
}
});
});
describe("api", async () => {
it("uses arguments to send to a slack api method", async () => {
const apis = sinon.stub().resolves({ ok: true });
const constructors = sinon
.stub(mocks.webapi, "WebClient")
.returns({ apiCall: apis });
/**
* @type {Config}
*/
const config = {
content: {
values: {
channel: "CHANNELHERE",
timestamp: "1234567890.000000",
},
},
core: mocks.core,
logger: new Logger(mocks.core).logger,
inputs: {
method: "pins.add",
token: "xoxb-example-002",
},
webapi: mocks.webapi,
};
await new Client().post(config);
assert.ok(constructors.calledWithNew());
assert.ok(
constructors.calledWith("xoxb-example-002", {
agent: undefined,
allowAbsoluteUrls: false,
logger: config.logger,
retryConfig: webapi.retryPolicies.fiveRetriesInFiveMinutes,
slackApiUrl: undefined,
}),
);
assert.ok(apis.calledOnce);
assert.ok(
apis.calledWith("pins.add", {
channel: "CHANNELHERE",
timestamp: "1234567890.000000",
}),
);
assert.ok(config.core.setOutput.calledWith("ok", true));
});
it("uses arguments to send to a custom api method", async () => {
const apis = sinon.stub().resolves({ done: true, response: "Infinite" });
const constructors = sinon
.stub(mocks.webapi, "WebClient")
.returns({ apiCall: apis });
/**
* @type {Config}
*/
const config = {
content: {
values: {
model: "llama3.2",
prompt: "How many sides does a circle have?",
stream: false,
},
},
core: mocks.core,
logger: new Logger(mocks.core).logger,
inputs: {
api: "http://localhost:11434/api/",
method: "generate",
retries: "10",
token: "ollamapassword",
},
webapi: mocks.webapi,
};
await new Client().post(config);
assert.ok(constructors.calledWithNew());
assert.ok(
constructors.calledWith("ollamapassword", {
agent: undefined,
allowAbsoluteUrls: false,
logger: config.logger,
retryConfig: webapi.retryPolicies.tenRetriesInAboutThirtyMinutes,
slackApiUrl: "http://localhost:11434/api/",
}),
);
assert.ok(apis.calledOnce);
assert.ok(
apis.calledWith("generate", {
model: "llama3.2",
prompt: "How many sides does a circle have?",
stream: false,
}),
);
assert.ok(config.core.setOutput.calledWith("ok", undefined));
assert.ok(
config.core.setOutput.calledWith(
"response",
JSON.stringify({ done: true, response: "Infinite" }),
),
);
});
});
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.calls.resolves(response);
await send(mocks.core);
assert.deepEqual(mocks.calls.getCall(0).firstArg, "chat.postMessage");
assert.deepEqual(mocks.calls.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 (err) {
console.error(err);
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.calls.resolves(response);
await send(mocks.core);
assert.deepEqual(mocks.calls.getCall(0).firstArg, "chat.postMessage");
assert.deepEqual(mocks.calls.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 (err) {
console.error(err);
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.calls.resolves(response);
await send(mocks.core);
assert.deepEqual(mocks.calls.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 (err) {
console.error(err);
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.calls.rejects(errors.requestErrorWithOriginal(response, true));
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.ok(mocks.core.setFailed.called);
assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok");
assert.equal(mocks.core.setOutput.getCall(0).lastArg, false);
assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response");
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.calls.rejects(errors.httpErrorFromResponse(response));
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.strictEqual(mocks.core.setFailed.called, false);
assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok");
assert.equal(mocks.core.setOutput.getCall(0).lastArg, false);
assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response");
response.body = response.data;
response.data = undefined;
assert.deepEqual(
mocks.core.setOutput.getCall(1).lastArg,
JSON.stringify(response),
);
assert.equal(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.calls.rejects(errors.platformErrorFromResult(response));
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.ok(mocks.core.setFailed.called);
assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok");
assert.equal(mocks.core.setOutput.getCall(0).lastArg, false);
assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response");
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.calls.rejects(errors.platformErrorFromResult(response));
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.strictEqual(mocks.core.setFailed.called, false);
assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok");
assert.equal(mocks.core.setOutput.getCall(0).lastArg, false);
assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response");
assert.deepEqual(
mocks.core.setOutput.getCall(1).lastArg,
JSON.stringify(response),
);
assert.equal(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.calls.rejects(errors.rateLimitedErrorWithDelay(12));
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.strictEqual(mocks.core.setFailed.called, false);
assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok");
assert.equal(mocks.core.setOutput.getCall(0).lastArg, false);
assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response");
assert.deepEqual(
mocks.core.setOutput.getCall(1).lastArg,
JSON.stringify(response),
);
assert.equal(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.notStrictEqual(proxying, false);
});
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.ok(
err.message.includes("Failed to configure the HTTPS proxy"),
);
} else {
assert.fail(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);
});
});
});
-271
View File
@@ -1,271 +0,0 @@
import assert from "node:assert";
import { beforeEach, describe, it } from "node:test";
import webapi from "@slack/web-api";
import sinon from "sinon";
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.getInput.withArgs("api").returns("http://localhost:8080");
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.api, "http://localhost:8080");
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");
assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called);
});
it("allows token environment variables with a webhook", async () => {
process.env.SLACK_TOKEN = "xoxb-example";
mocks.core.getInput.withArgs("webhook").returns("https://example.com");
mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook");
const config = new Config(mocks.core);
assert.equal(config.inputs.token, "xoxb-example");
assert.equal(config.inputs.webhook, "https://example.com");
assert.equal(config.inputs.webhookType, "incoming-webhook");
assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called);
assert.ok(mocks.core.setSecret.withArgs("https://example.com").called);
});
it("allows webhook environment variables with a token", async () => {
process.env.SLACK_WEBHOOK_URL = "https://example.com";
mocks.core.getInput.withArgs("method").returns("chat.postMessage");
mocks.core.getInput.withArgs("token").returns("xoxb-example");
const config = new Config(mocks.core);
assert.equal(config.inputs.method, "chat.postMessage");
assert.equal(config.inputs.token, "xoxb-example");
assert.equal(config.inputs.webhook, "https://example.com");
assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called);
assert.ok(mocks.core.setSecret.withArgs("https://example.com").called);
});
it("errors when both the token and webhook is provided", async () => {
mocks.core.getInput.withArgs("token").returns("xoxb-example");
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.ok(
err.message.includes(
"Invalid input! Either the token or webhook is required - not both.",
),
);
assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called);
assert.ok(
mocks.core.setSecret.withArgs("https://example.com").called,
);
} else {
assert.fail(err);
}
}
});
it("errors if the method is provided without a token", async () => {
mocks.core.getInput.withArgs("method").returns("chat.postMessage");
try {
new Config(mocks.core);
assert.fail("Failed to error when invalid inputs are provided");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Missing input! A token must be provided to use the method decided.",
),
);
} else {
assert.fail(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.ok(
err.message.includes(
"Missing input! Either a method or webhook is required to take action.",
),
);
} else {
assert.fail(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.ok(
err.message.includes(
"Missing input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.",
),
);
} else {
assert.fail(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.ok(
err.message.includes(
"Invalid input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.",
),
);
} else {
assert.fail(err);
}
}
});
});
describe("instrument", () => {
it("adds metadata to webapi with package name and version", async () => {
const stub = sinon.stub();
const original = Object.getOwnPropertyDescriptor(
webapi,
"addAppMetadata",
);
Object.defineProperty(webapi, "addAppMetadata", {
value: stub,
configurable: true,
});
try {
mocks.core.getInput.withArgs("method").returns("chat.postMessage");
mocks.core.getInput.withArgs("token").returns("xoxb-example");
new Config(mocks.core);
assert.ok(stub.calledOnce);
const { name, version } = stub.firstCall.args[0];
assert.equal(name, "@slack/slack-github-action");
assert.ok(version);
} finally {
Object.defineProperty(webapi, "addAppMetadata", original);
}
});
it("adds metadata to webhook with package name and version", async () => {
mocks.core.getInput.withArgs("method").returns("chat.postMessage");
mocks.core.getInput.withArgs("token").returns("xoxb-example");
const config = new Config(mocks.core);
assert.ok(
config.axios.defaults.headers.common["User-Agent"].startsWith(
"@slack:slack-github-action/",
),
);
assert.ok(
config.axios.defaults.headers.common["User-Agent"].length >
"@slack:slack-github-action/".length,
);
});
});
describe("mask", 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.ok(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.ok(mocks.core.setSecret.withArgs("https://slack.com").called);
}
});
});
describe("validate", () => {
it('allow the "retries" option with lowercased space', async () => {
mocks.axios.post.returns(Promise.resolve("LGTM"));
mocks.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.ok(
err.message.includes(
'Invalid input! An unknown "retries" value was used: FOREVER',
),
);
} else {
assert.fail(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.ok(
err.message.includes(
'Invalid input! An unknown "retries" value was used: FOREVER',
),
);
} else {
assert.fail(err);
}
}
});
});
});
-647
View File
@@ -1,647 +0,0 @@
import assert from "node:assert";
import path from "node:path";
import { beforeEach, describe, it } from "node:test";
import { YAMLException } from "js-yaml";
import Config from "../src/config.js";
import Content from "../src/content.js";
import SlackError from "../src/errors.js";
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.ok(
err.message.includes(
"Invalid input! Just the payload or payload file path is required.",
),
);
} else {
assert.fail(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 requires configuration", async () => {
mocks.core.getInput.withArgs("payload").returns(`{
"message": "this matches an existing variable: \${{ github.apiUrl }}",
"channel": "C0123456789"
}
`);
const config = new Config(mocks.core);
// biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax
const expected = {
message: "this matches an existing variable: ${{ github.apiUrl }}",
channel: "C0123456789",
};
// biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values
assert.deepEqual(config.content.values, expected);
});
it("templatizes variables with matching variables", async () => {
mocks.core.getInput.withArgs("payload").returns(`
channel: C0123456789
reply_broadcast: false
message: Served \${{ env.NUMBER }} items
blocks:
- type: section
text:
type: mrkdwn
text: "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}"
- type: divider
- type: section
block_id: selector
text:
type: mrkdwn
text: Send feedback
accessory:
action_id: response
type: multi_static_select
placeholder:
type: plain_text
text: Select URL
options:
- text:
type: plain_text
text: "\${{ github.apiUrl }}"
value: api
- text:
type: plain_text
text: "\${{ github.serverUrl }}"
value: server
- text:
type: plain_text
text: "\${{ github.graphqlUrl }}"
value: graphql
`);
mocks.core.getBooleanInput.withArgs("payload-templated").returns(true);
process.env.DETAILS = `
-fri
-sat
-sun`;
process.env.NUMBER = 12;
const config = new Config(mocks.core);
process.env.DETAILS = undefined;
process.env.NUMBER = undefined;
const expected = {
channel: "C0123456789",
reply_broadcast: false,
message: "Served 12 items",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "Served 12 items on: \n-fri\n-sat\n-sun",
},
},
{
type: "divider",
},
{
type: "section",
block_id: "selector",
text: {
type: "mrkdwn",
text: "Send feedback",
},
accessory: {
action_id: "response",
type: "multi_static_select",
placeholder: {
type: "plain_text",
text: "Select URL",
},
options: [
{
text: {
type: "plain_text",
text: "https://api.github.com",
},
value: "api",
},
{
text: {
type: "plain_text",
text: "https://github.com",
},
value: "server",
},
{
text: {
type: "plain_text",
text: "https://api.github.com/graphql",
},
value: "graphql",
},
],
},
},
],
};
assert.deepEqual(config.content.values, expected);
});
/**
* @see {@link https://github.com/slackapi/slack-github-action/issues/203}
*/
it("templatizes variables with missing variables", async () => {
// biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax
mocks.core.getInput
.withArgs("payload")
.returns("message: What makes ${{ env.TREASURE }} a secret");
// biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values
mocks.core.getBooleanInput.withArgs("payload-templated").returns(true);
const config = new Config(mocks.core);
const expected = {
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: mocks.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.ok(
err.message.includes(
"Invalid input! No payload content was provided",
),
);
} else {
assert.fail(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.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload",
),
);
assert.notStrictEqual(err.cause?.values, undefined);
assert.equal(err.cause.values.length, 2);
const [jsonError, yamlError] = err.cause.values;
assert.ok(jsonError instanceof SyntaxError);
assert.ok(yamlError instanceof YAMLException);
} else {
assert.fail(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 requires configuration", async () => {
mocks.core.getInput.withArgs("payload-file-path").returns("example.json");
mocks.fs.readFileSync
.withArgs(path.resolve("example.json"), "utf-8")
.returns(`{
"message": "this matches an existing variable: \${{ github.apiUrl }}",
"channel": "C0123456789"
}
`);
const config = new Config(mocks.core);
// biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax
const expected = {
message: "this matches an existing variable: ${{ github.apiUrl }}",
channel: "C0123456789",
};
// biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values
assert.deepEqual(config.content.values, expected);
});
it("templatizes variables with matching variables", async () => {
mocks.core.getInput.withArgs("payload-file-path").returns("example.json");
mocks.fs.readFileSync
.withArgs(path.resolve("example.json"), "utf-8")
.returns(`{
"channel": "C0123456789",
"reply_broadcast": false,
"message": "Served \${{ env.NUMBER }} items",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}"
}
},
{
"type": "divider"
},
{
"type": "section",
"block_id": "selector",
"text": {
"type": "mrkdwn",
"text": "Send feedback"
},
"accessory": {
"action_id": "response",
"type": "multi_static_select",
"placeholder": {
"type": "plain_text",
"text": "Select URL"
},
"options": [
{
"text": {
"type": "plain_text",
"text": "\${{ github.apiUrl }}"
},
"value": "api"
},
{
"text": {
"type": "plain_text",
"text": "\${{ github.serverUrl }}"
},
"value": "server"
},
{
"text": {
"type": "plain_text",
"text": "\${{ github.graphqlUrl }}"
},
"value": "graphql"
}
]
}
}
]
}`);
mocks.core.getBooleanInput.withArgs("payload-templated").returns(true);
process.env.DETAILS = `
-fri
-sat
-sun`;
process.env.NUMBER = 12;
const config = new Config(mocks.core);
process.env.DETAILS = undefined;
process.env.NUMBER = undefined;
const expected = {
channel: "C0123456789",
reply_broadcast: false,
message: "Served 12 items",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "Served 12 items on: \n-fri\n-sat\n-sun",
},
},
{
type: "divider",
},
{
type: "section",
block_id: "selector",
text: {
type: "mrkdwn",
text: "Send feedback",
},
accessory: {
action_id: "response",
type: "multi_static_select",
placeholder: {
type: "plain_text",
text: "Select URL",
},
options: [
{
text: {
type: "plain_text",
text: "https://api.github.com",
},
value: "api",
},
{
text: {
type: "plain_text",
text: "https://github.com",
},
value: "server",
},
{
text: {
type: "plain_text",
text: "https://api.github.com/graphql",
},
value: "graphql",
},
],
},
},
],
};
assert.deepEqual(config.content.values, expected);
});
/**
* @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: mocks.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.ok(
err.message.includes("Invalid input! No payload found for content"),
);
} else {
assert.fail(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.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload file",
),
);
} else {
assert.fail(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.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload file",
),
);
assert.notStrictEqual(err.cause?.values, undefined);
assert.equal(err.cause.values.length, 1);
assert.ok(
err.cause.values[0].message.includes(
"Invalid input! Failed to parse file extension unknown.md",
),
);
} else {
assert.fail(err);
}
}
});
it("fails if invalid JSON exists in the input payload", async () => {
mocks.core.getInput.withArgs("payload-file-path").returns("example.json");
mocks.fs.readFileSync
.withArgs(path.resolve("example.json"), "utf-8")
.returns(`{
"message": "a truncated file without an end`);
try {
await send(mocks.core);
assert.fail("Failed to throw for invalid JSON");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload file",
),
);
assert.notStrictEqual(err.cause?.values, undefined);
assert.equal(err.cause.values.length, 1);
assert.ok(err.cause.values[0] instanceof SyntaxError);
} else {
assert.fail(err);
}
}
});
it("fails if invalid YAML exists in the input payload", async () => {
mocks.core.getInput.withArgs("payload-file-path").returns("example.yaml");
mocks.fs.readFileSync
.withArgs(path.resolve("example.yaml"), "utf-8")
.returns(`- "message": "assigned": "values"`);
try {
await send(mocks.core);
assert.fail("Failed to throw for invalid YAML");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload file",
),
);
assert.notStrictEqual(err.cause?.values, undefined);
assert.equal(err.cause.values.length, 1);
assert.ok(err.cause.values[0] instanceof YAMLException);
} else {
assert.fail(err);
}
}
});
});
});
-102
View File
@@ -1,102 +0,0 @@
import fs from "node:fs";
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.axios = this.sandbox.stub(axios);
this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall");
this.core = {
debug: this.sandbox.stub(),
error: this.sandbox.stub(),
getInput: this.sandbox.stub(),
getBooleanInput: this.sandbox.stub(),
info: this.sandbox.stub(),
isDebug: this.sandbox.stub(),
setFailed: this.sandbox.stub(),
setOutput: this.sandbox.stub(),
setSecret: this.sandbox.stub(),
warning: this.sandbox.stub(),
};
this.fs = this.sandbox.stub(fs);
this.webapi = {
WebClient: function () {
this.apiCall = () => ({
ok: true,
});
},
};
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.axios.post.resetHistory();
this.calls.resetHistory();
this.core.debug.reset();
this.core.error.reset();
this.core.getInput.reset();
this.core.getBooleanInput.reset();
this.core.info.reset();
this.core.isDebug.reset();
this.core.setFailed.reset();
this.core.setOutput.reset();
this.core.setSecret.reset();
this.core.warning.reset();
this.webapi = {
WebClient: function () {
this.apiCall = () => ({
ok: true,
});
},
};
this.core.getInput.withArgs("errors").returns("false");
this.core.getInput.withArgs("retries").returns("5");
process.env.SLACK_TOKEN = "";
process.env.SLACK_WEBHOOK_URL = "";
}
}
export const mocks = new Mock();
-29
View File
@@ -1,29 +0,0 @@
import assert from "node:assert";
import { beforeEach, describe, it } from "node:test";
import { LogLevel } from "@slack/logger";
import Logger from "../src/logger.js";
import { mocks } from "./index.spec.js";
describe("logger", () => {
beforeEach(() => {
mocks.reset();
});
describe("level", () => {
it("debug", () => {
mocks.core.isDebug.returns(true);
const { logger } = new Logger(mocks.core);
const actual = logger.getLevel();
const expected = LogLevel.DEBUG;
assert.strictEqual(actual, expected);
});
it("info", () => {
mocks.core.isDebug.returns(false);
const { logger } = new Logger(mocks.core);
const actual = logger.getLevel();
const expected = LogLevel.INFO;
assert.strictEqual(actual, expected);
});
});
});
-80
View File
@@ -1,80 +0,0 @@
import assert from "node:assert";
import { beforeEach, describe, it } from "node:test";
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.ok(mocks.core.setOutput.getCall(2).lastArg >= 0);
});
it("token", async () => {
process.env.SLACK_WEBHOOK_URL = "https://example.com"; // https://github.com/slackapi/slack-github-action/issues/373
mocks.calls.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.ok(mocks.core.setOutput.getCall(2).lastArg >= 0);
});
it("incoming webhook", async () => {
process.env.SLACK_TOKEN = "xoxb-example"; // https://github.com/slackapi/slack-github-action/issues/373
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.ok(mocks.core.setOutput.getCall(2).lastArg >= 0);
});
});
});
-346
View File
@@ -1,346 +0,0 @@
import assert from "node:assert";
import { beforeEach, describe, it } from "node:test";
import { AxiosError } from "axios";
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: mocks.core,
inputs: {},
};
try {
await new Webhook().post(config);
assert.fail("Failed to throw for missing input");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(err.message.includes("No webhook was provided to post to"));
} else {
assert.fail(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.ok(
err.message.includes("Request failed with status code 400"),
);
} else {
assert.fail(err);
}
}
assert.equal(mocks.axios.post.getCalls().length, 1);
const [url, payload, options] = mocks.axios.post.getCall(0).args;
assert.equal(url, "https://hooks.slack.com");
assert.deepEqual(payload, { drinks: "coffee" });
assert.deepEqual(options, {});
assert.equal(mocks.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.ok(
err.message.includes("Request failed with status code 400"),
);
} else {
assert.fail(err);
}
}
assert.equal(mocks.axios.post.getCalls().length, 1);
const [url, payload, options] = mocks.axios.post.getCall(0).args;
assert.equal(url, "https://hooks.slack.com");
assert.deepEqual(payload, { textt: "oops" });
assert.deepEqual(options, {});
assert.equal(mocks.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: mocks.core,
inputs: {},
};
try {
new Webhook().proxies(config);
assert.fail("Failed to throw for missing input");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes("No webhook was provided to proxy to"),
);
} else {
assert.fail(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.strictEqual(request, undefined);
});
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.notStrictEqual(proxying, false);
});
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.strictEqual(proxying, false);
});
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.ok(
err.message.includes("Failed to configure the HTTPS proxy"),
);
} else {
assert.fail(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.ok(
err.message.includes("Failed to configure the HTTPS proxy"),
);
assert.ok(err.cause.message.includes("Unsupported URL protocol"));
} else {
assert.fail(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.ok(
result.retryDelay(10, mocks.errors.axios.network_failed) > 1800000,
"last attempt is around 30 minutes after starting",
);
assert.ok(
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",
);
});
});
});