chore: release
This commit is contained in:
@@ -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).
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,59 +0,0 @@
|
||||
# Contributors Guide
|
||||
|
||||
Interested in contributing? Awesome! Before you do though, please read our
|
||||
[Code of Conduct](https://slackhq.github.io/code-of-conduct). We take it very seriously, and expect that you will as
|
||||
well.
|
||||
|
||||
There are many ways you can contribute! :heart:
|
||||
|
||||
### Bug Reports and Fixes :bug:
|
||||
- If you find a bug, please [create a new issue](https://github.com/slackapi/slack-github-action/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still
|
||||
be reviewed.
|
||||
- Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`.
|
||||
- If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number.
|
||||
- Include tests that isolate the bug and verifies that it was fixed.
|
||||
|
||||
### New Features :bulb:
|
||||
- If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/slackapi/slack-github-action/issues/new).
|
||||
- Issues that have been identified as a feature request will be labelled `enhancement`.
|
||||
- If you'd like to implement the new feature, please wait for feedback from the project
|
||||
maintainers before spending too much time writing the code. In some cases, `enhancement`s may
|
||||
not align well with the project objectives at the time.
|
||||
|
||||
### Tests :mag:, Documentation :books:, Miscellaneous :sparkles:
|
||||
- If you'd like to improve the tests, you want to make the documentation clearer, you have an
|
||||
alternative implementation of something that may have advantages over the way its currently
|
||||
done, or you have any other change, we would be happy to hear about it!
|
||||
- If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind.
|
||||
- If not, [open an Issue](https://github.com/slackapi/slack-github-actiondcjtfkgitihftbgeungvnhgbvkhjtdbiilknucucrfek/issues/new) to discuss the idea first.
|
||||
|
||||
If you're new to our project and looking for some way to make your first contribution, look for
|
||||
Issues labelled `good first contribution`.
|
||||
|
||||
## Requirements
|
||||
|
||||
For your contribution to be accepted:
|
||||
|
||||
- [x] You must have signed the [Contributor License Agreement (CLA)](https://cla.salesforce.com/sign-cla).
|
||||
- [x] The test suite must be complete and pass.
|
||||
- [x] The changes must be approved by code review.
|
||||
- [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number.
|
||||
|
||||
If the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created.
|
||||
|
||||
[Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z)
|
||||
|
||||
## Creating a Pull Request
|
||||
|
||||
1. :fork_and_knife: Fork the repository on GitHub.
|
||||
2. :runner: Clone/fetch your fork to your local development machine. It's a good idea to run the tests just
|
||||
to make sure everything is in order.
|
||||
3. :herb: Create a new branch and check it out.
|
||||
4. :crystal_ball: Make your changes and commit them locally. Magic happens here!
|
||||
5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-16`).
|
||||
6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `master` in this
|
||||
repository.
|
||||
|
||||
## Maintainers
|
||||
|
||||
There are more details about processes and workflow in the [Maintainer's Guide](./maintainers_guide.md).
|
||||
@@ -1,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"
|
||||
@@ -1,49 +0,0 @@
|
||||
### Description
|
||||
|
||||
Describe your issue here.
|
||||
|
||||
### What type of issue is this? (place an `x` in one of the `[ ]`)
|
||||
- [ ] bug
|
||||
- [ ] enhancement (feature request)
|
||||
- [ ] question
|
||||
- [ ] documentation related
|
||||
- [ ] example code related
|
||||
- [ ] testing related
|
||||
- [ ] discussion
|
||||
|
||||
### Requirements (place an `x` in each of the `[ ]`)
|
||||
* [ ] I've read and understood the [Contributing guidelines](https://github.com/slackapi/slack-github-action/blob/main/.github/contributing.md) and have done my best effort to follow them.
|
||||
* [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
|
||||
* [ ] I've searched for any related issues and avoided creating a duplicate issue.
|
||||
|
||||
---
|
||||
|
||||
### Bug Report
|
||||
|
||||
Filling out the following details about bugs will help us solve your issue sooner.
|
||||
|
||||
#### Reproducible in:
|
||||
|
||||
package version:
|
||||
|
||||
node version:
|
||||
|
||||
OS version(s):
|
||||
|
||||
#### Steps to reproduce:
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
#### Expected result:
|
||||
|
||||
What you expected to happen
|
||||
|
||||
#### Actual result:
|
||||
|
||||
What actually happened
|
||||
|
||||
#### Attachments:
|
||||
|
||||
Logs, screenshots, screencast, sample project, funny gif, etc.
|
||||
@@ -1,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.
|
||||
@@ -1,8 +0,0 @@
|
||||
### Summary
|
||||
|
||||
Describe the goal of this PR. Mention any related Issue numbers.
|
||||
|
||||
### Requirements <!-- Place an `x` in each `[ ]` -->
|
||||
|
||||
- [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/slack-github-action/blob/main/.github/contributing.md) and have done my best effort to follow them.
|
||||
- [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"pull_request": {
|
||||
"title": "Bump actions/checkout from 3 to 4",
|
||||
"html_url": "https://github.com/slackapi/slack-github-action/pull/238"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "slackapi/slack-github-action"
|
||||
},
|
||||
"sender": {
|
||||
"login": "dependabot"
|
||||
}
|
||||
}
|
||||
@@ -1,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
|
||||
@@ -1,6 +0,0 @@
|
||||
# Application values
|
||||
.env.*
|
||||
.env
|
||||
!.env.example
|
||||
.slack/apps.json
|
||||
.slack/apps.*.json
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"source": "local"
|
||||
},
|
||||
"project_id": "c4805b41-d1ce-4ea0-b297-ed2f8c64c267"
|
||||
}
|
||||
|
||||
@@ -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 }}>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"_metadata": {
|
||||
"major_version": 2
|
||||
},
|
||||
"display_information": {
|
||||
"name": "slacktion",
|
||||
"description": "an application for developing the Slack GitHub Action"
|
||||
},
|
||||
"features": {
|
||||
"app_home": {
|
||||
"messages_tab_enabled": true,
|
||||
"messages_tab_read_only_enabled": true
|
||||
},
|
||||
"bot_user": {
|
||||
"display_name": "slacktion"
|
||||
}
|
||||
},
|
||||
"oauth_config": {
|
||||
"scopes": {
|
||||
"bot": [
|
||||
"chat:write",
|
||||
"chat:write.public",
|
||||
"files:write",
|
||||
"incoming-webhook",
|
||||
"reactions:write"
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"org_deploy_enabled": true,
|
||||
"incoming_webhooks": {
|
||||
"incoming_webhooks_enabled": true
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
"event_notifications": {
|
||||
"title": "GitHub event notifications",
|
||||
"description": "A new changeset has been discovered",
|
||||
"input_parameters": {
|
||||
"properties": {
|
||||
"author": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel_id": {
|
||||
"type": "slack#/types/channel_id"
|
||||
},
|
||||
"repo_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"event_url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["author", "channel_id", "repo_name", "status", "event_url"]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "0",
|
||||
"function_id": "slack#/functions/send_message",
|
||||
"inputs": {
|
||||
"channel_id": "{{inputs.channel_id}}",
|
||||
"message": ":ship: {{inputs.repo_name}}(@{{inputs.author}}): `{{inputs.status}}` <{{inputs.event_url}}|Check it out here>!"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"type": "webhook",
|
||||
"name": "Post the latest actions from GitHub",
|
||||
"description": "Share event information to a channel",
|
||||
"workflow": "#/workflows/event_notifications",
|
||||
"inputs": {
|
||||
"author": {
|
||||
"value": "{{data.author}}"
|
||||
},
|
||||
"channel_id": {
|
||||
"value": "{{data.channel_id}}"
|
||||
},
|
||||
"repo_name": {
|
||||
"value": "{{data.repo_name}}"
|
||||
},
|
||||
"status": {
|
||||
"value": "{{data.status}}"
|
||||
},
|
||||
"event_url": {
|
||||
"value": "{{data.event_url}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,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
|
||||
@@ -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 }}
|
||||
@@ -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."
|
||||
@@ -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 }}
|
||||
@@ -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 }}"
|
||||
@@ -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
@@ -1,11 +0,0 @@
|
||||
# Build artifacts
|
||||
dist
|
||||
|
||||
# Development dependencies
|
||||
node_modules
|
||||
|
||||
# Extra files
|
||||
.DS_Store
|
||||
|
||||
# Testing remnants
|
||||
coverage.txt
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+63286
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1521
File diff suppressed because it is too large
Load Diff
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -1,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"
|
||||
]
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 }}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
-2370
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -1,123 +0,0 @@
|
||||
import axiosRetry, { exponentialDelay, linearDelay } from "axios-retry";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import Config from "./config.js";
|
||||
import SlackError from "./errors.js";
|
||||
|
||||
/**
|
||||
* This Webhook class posts the configured payload to the provided webhook, with
|
||||
* whatever additional settings set.
|
||||
*/
|
||||
export default class Webhook {
|
||||
/**
|
||||
* @param {Config} config
|
||||
*/
|
||||
async post(config) {
|
||||
if (!config.inputs.webhook) {
|
||||
throw new SlackError(config.core, "No webhook was provided to post to");
|
||||
}
|
||||
/**
|
||||
* @type {import("axios-retry").IAxiosRetryConfig}
|
||||
* @see {@link https://www.npmjs.com/package/axios-retry}
|
||||
*/
|
||||
const retries = this.retries(config.inputs.retries);
|
||||
axiosRetry(config.axios, retries);
|
||||
try {
|
||||
const response = await config.axios.post(
|
||||
config.inputs.webhook,
|
||||
config.content.values,
|
||||
{
|
||||
...this.proxies(config),
|
||||
},
|
||||
);
|
||||
config.core.setOutput("ok", response.status === 200);
|
||||
config.core.setOutput("response", JSON.stringify(response.data));
|
||||
config.core.debug(JSON.stringify(response.data));
|
||||
} catch (/** @type {any} */ err) {
|
||||
const response = err.toJSON();
|
||||
config.core.setOutput("ok", response.status === 200);
|
||||
config.core.setOutput("response", JSON.stringify(response.message));
|
||||
config.core.debug(response);
|
||||
throw new SlackError(config.core, response.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return configurations for http proxy options if these are set.
|
||||
* @param {Config} config
|
||||
* @returns {import("axios").AxiosRequestConfig | undefined}
|
||||
* @see {@link https://github.com/slackapi/slack-github-action/pull/132}
|
||||
*/
|
||||
proxies(config) {
|
||||
const { webhook, proxy } = config.inputs;
|
||||
if (!webhook) {
|
||||
throw new SlackError(config.core, "No webhook was provided to proxy to");
|
||||
}
|
||||
if (!proxy) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
if (new URL(webhook).protocol !== "https:") {
|
||||
config.core.debug(
|
||||
"The webhook destination is not HTTPS so skipping the HTTPS proxy",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
switch (new URL(proxy).protocol) {
|
||||
case "https:":
|
||||
return {
|
||||
httpsAgent: new HttpsProxyAgent(proxy),
|
||||
};
|
||||
case "http:":
|
||||
return {
|
||||
httpsAgent: new HttpsProxyAgent(proxy),
|
||||
proxy: false,
|
||||
};
|
||||
default:
|
||||
throw new SlackError(
|
||||
config.core,
|
||||
`Unsupported URL protocol: ${proxy}`,
|
||||
);
|
||||
}
|
||||
} catch (/** @type {any} */ err) {
|
||||
throw new SlackError(config.core, "Failed to configure the HTTPS proxy", {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return configurations for retry options with different delays.
|
||||
* @param {string} option
|
||||
* @returns {import("axios-retry").IAxiosRetryConfig}
|
||||
*/
|
||||
retries(option) {
|
||||
switch (option?.trim().toUpperCase()) {
|
||||
case "0":
|
||||
return { retries: 0 };
|
||||
case "5":
|
||||
return {
|
||||
retryCondition: axiosRetry.isRetryableError,
|
||||
retries: 5,
|
||||
retryDelay: linearDelay(60 * 1000), // 5 minutes
|
||||
};
|
||||
case "10":
|
||||
return {
|
||||
retryCondition: axiosRetry.isRetryableError,
|
||||
retries: 10,
|
||||
retryDelay: (count, err) => exponentialDelay(count, err, 2 * 1000), // 34.12 minutes
|
||||
};
|
||||
case "RAPID":
|
||||
return {
|
||||
retryCondition: axiosRetry.isRetryableError,
|
||||
retries: 12,
|
||||
retryDelay: linearDelay(1 * 1000), // 12 seconds
|
||||
};
|
||||
default:
|
||||
return {
|
||||
retryCondition: axiosRetry.isRetryableError,
|
||||
retries: 5,
|
||||
retryDelay: linearDelay(60 * 1000), // 5 minutes
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user