feat: add manual dispatch support for GitHub Actions to test Discord webhook integration with optional release inputs. resolves #52

This commit is contained in:
SethCohen
2026-04-03 12:42:38 -04:00
parent 80aca15d72
commit e15eb81a91
5 changed files with 224 additions and 25 deletions
+64 -15
View File
@@ -68,7 +68,51 @@ jobs:
reduce_headings: true
```
### 3. Add Your Webhook URL as a Secret
### 3. Optional: Test with `workflow_dispatch`
If you want to test the action manually without relying on cloning and using `act`, you can add `workflow_dispatch` and pass release fields into the action:
Example inputs:
```yaml
release_name: v1.2.3
release_body: |
## Changes
- Added manual testing support
- Verified Discord webhook output
release_html_url: https://github.com/owner/repo/releases/tag/v1.2.3
```
```yaml
on:
release:
types: [published]
workflow_dispatch:
inputs:
release_name:
description: Release title to post
required: true
release_body:
description: Release notes body to post
required: true
release_html_url:
description: Release URL to link in Discord
required: false
jobs:
github-releases-to-discord:
runs-on: ubuntu-latest
steps:
- name: GitHub Releases to Discord
uses: SethCohen/github-releases-to-discord@v1
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
release_name: ${{ inputs.release_name }}
release_body: ${{ inputs.release_body }}
release_html_url: ${{ inputs.release_html_url }}
```
### 4. Add Your Webhook URL as a Secret
- In your GitHub repo, go to **Settings → Secrets and variables → Actions**.
- Add a new secret named `WEBHOOK_URL` and paste your Discord webhook URL.
@@ -77,20 +121,23 @@ jobs:
## Configuration Options
| Input Name | Required | Default | Description |
|-------------------------------|----------|-------------|--------------------------------------------------------------------|
| `webhook_url` | ✔ | | Discord webhook URL (use a GitHub secret). |
| `color` | ❌ | 2105893 | Embed color (decimal). |
| `username` | ❌ | | Webhook username. |
| `avatar_url` | ❌ | | Webhook avatar image URL. |
| `custom_html_url` | ❌ | | Custom URL for the embed title (overrides GitHub release URL). |
| `content` | ❌ | | Additional message content (e.g., `@everyone`). |
| `footer_title` | ❌ | | Footer title. |
| `footer_icon_url` | ❌ | | Footer icon image URL. |
| `footer_timestamp` | ❌ | false | Show timestamp in footer (`true`/`false`). |
| `max_description` | ❌ | 4096 | Max description length (Discord limit: 4096). |
| `remove_github_reference_links`| ❌ | false | Remove PR, commit, and issue links from the description. |
| `reduce_headings` | ❌ | false | Reduce heading sizes for compact display. |
| Input Name | Required | Default | Description |
|--------------------------------|----------|-------------|--------------------------------------------------------------------|
| `webhook_url` | ✔ | | Discord webhook URL (use a GitHub secret). |
| `color` | ❌ | 2105893 | Embed color (decimal). |
| `username` | ❌ | | Webhook username. |
| `avatar_url` | ❌ | | Webhook avatar image URL. |
| `custom_html_url` | ❌ | | Custom URL for the embed title (overrides GitHub release URL). |
| `content` | ❌ | | Additional message content (e.g., `@everyone`). |
| `release_name` | ❌ | | Manual release title for `workflow_dispatch` testing. |
| `release_body` | ❌ | | Manual release body for `workflow_dispatch` testing. |
| `release_html_url` | ❌ | | Manual release URL for `workflow_dispatch` testing. |
| `footer_title` | ❌ | | Footer title. |
| `footer_icon_url` | ❌ | | Footer icon image URL. |
| `footer_timestamp` | ❌ | false | Show timestamp in footer (`true`/`false`). |
| `max_description` | ❌ | 4096 | Max description length (Discord limit: 4096). |
| `remove_github_reference_links`| ❌ | false | Remove PR, commit, and issue links from the description. |
| `reduce_headings` | ❌ | false | Reduce heading sizes for compact display. |
---
@@ -111,6 +158,8 @@ jobs:
- Use Markdown in your release notes for best results.
- **Private Repos:**
- The action works for both public and private repositories.
- **Manual Testing:**
- Use `release_name`, `release_body`, and optionally `release_html_url` when triggering the workflow with `workflow_dispatch`.
---
+9
View File
@@ -21,6 +21,15 @@ inputs:
content:
description: String content for webhook.
required: false
release_name:
description: Optional release title used when manually triggering the workflow with workflow_dispatch.
required: false
release_body:
description: Optional release body used when manually triggering the workflow with workflow_dispatch.
required: false
release_html_url:
description: Optional release URL used when manually triggering the workflow with workflow_dispatch.
required: false
footer_title:
description: Title for the footer.
required: false
+46 -9
View File
@@ -1,6 +1,7 @@
import core from '@actions/core';
import github from '@actions/github';
import fetch from 'node-fetch';
import { fileURLToPath } from 'node:url';
/**
* Removes carriage return characters.
@@ -148,11 +149,32 @@ const getMaxDescription = () => {
* @returns {object} The context with release details.
*/
const getContext = () => {
const { release } = github.context.payload;
return resolveReleaseContext(github.context.payload.release, {
body: core.getInput('release_body'),
name: core.getInput('release_name'),
html_url: core.getInput('release_html_url')
});
};
/**
* Resolves release data from either a GitHub release event or manual inputs.
* @param {object|null|undefined} release The release payload from the event.
* @param {object} manualInputs Manual fallback inputs.
* @returns {object} The resolved release context.
*/
const resolveReleaseContext = (release, manualInputs) => {
if (release) {
return {
body: release.body || '',
name: release.name || '',
html_url: release.html_url || ''
};
}
return {
body: release.body,
name: release.name,
html_url: release.html_url
body: manualInputs.body || '',
name: manualInputs.name || '',
html_url: manualInputs.html_url || ''
};
};
@@ -192,12 +214,15 @@ const limitString = (str, maxLength, url, clipAtLine = false) => {
const buildEmbedMessage = (name, html_url, description) => {
const embedMsg = {
title: limitString(name, 256),
url: html_url,
color: core.getInput('color'),
description: limitString(description, Math.min(getMaxDescription(), 6000 - name.length)),
footer: {}
};
if (html_url) {
embedMsg.url = html_url;
}
if (core.getInput('custom_html_url')) {
embedMsg.url = core.getInput('custom_html_url');
}
@@ -277,6 +302,11 @@ const run = async () => {
if (!webhookUrl) return core.setFailed('webhook_url not set.');
const { body, html_url, name } = getContext();
if (!body || !name) {
return core.setFailed('No GitHub release payload found. When using workflow_dispatch, pass release_name and release_body inputs to the action.');
}
const description = formatDescription(body);
const embedMsg = buildEmbedMessage(name, html_url, description);
@@ -286,9 +316,13 @@ const run = async () => {
await sendWebhook(webhookUrl, requestBody);
};
run()
.then(() => core.info('Action completed successfully'))
.catch(err => core.setFailed(err.message));
const isMainModule = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isMainModule) {
run()
.then(() => core.info('Action completed successfully'))
.catch(err => core.setFailed(err.message));
}
// Export utility functions for testing
export {
@@ -300,5 +334,8 @@ export {
reduceHeadings,
convertLinksToMarkdown,
limitString,
formatDescription
formatDescription,
resolveReleaseContext,
getContext,
run
};
+30 -1
View File
@@ -7,7 +7,8 @@ import {
reduceHeadings,
convertLinksToMarkdown,
limitString,
formatDescription
formatDescription,
resolveReleaseContext
} from '../index.js';
import { jest } from '@jest/globals';
@@ -94,6 +95,34 @@ describe('index.js utility functions', () => {
expect(convertLinksToMarkdown(input)).toBe('[already](https://github.com/owner/repo/pull/1) and [PR #2](https://github.com/owner/repo/pull/2)');
});
describe('resolveReleaseContext', () => {
test('uses release payload when present', () => {
const release = {
name: 'v1.2.3',
body: 'Release notes',
html_url: 'https://example.com/releases/v1.2.3'
};
expect(resolveReleaseContext(release, {
name: 'ignored',
body: 'ignored',
html_url: 'https://example.com/ignored'
})).toEqual(release);
});
test('falls back to manual inputs when release payload is missing', () => {
expect(resolveReleaseContext(null, {
name: 'manual release',
body: 'manual body',
html_url: 'https://example.com/manual'
})).toEqual({
name: 'manual release',
body: 'manual body',
html_url: 'https://example.com/manual'
});
});
});
describe('limitString', () => {
test('returns string unchanged if under maxLength', () => {
expect(limitString('short', 10)).toBe('short');
+75
View File
@@ -0,0 +1,75 @@
import { jest } from '@jest/globals';
const inputValues = {
webhook_url: 'https://discord.com/api/webhooks/test/webhook',
color: '2105893',
username: 'Release Changelog',
avatar_url: 'https://example.com/avatar.png',
content: '||@everyone||',
release_name: 'v1.2.3',
release_body: '## Changes\n- Added manual dispatch support\n- Verified webhook payload',
release_html_url: 'https://github.com/owner/repo/releases/tag/v1.2.3'
};
const fetchMock = jest.fn(async () => ({
ok: true,
status: 200,
json: async () => ({ id: 'discord-message-id' }),
headers: {
get: () => null
}
}));
const coreMock = {
getInput: jest.fn((name) => inputValues[name] || ''),
getBooleanInput: jest.fn((name) => false),
setFailed: jest.fn(),
info: jest.fn(),
warning: jest.fn()
};
const githubMock = {
context: {
payload: {}
}
};
jest.unstable_mockModule('@actions/core', () => ({
default: coreMock
}));
jest.unstable_mockModule('@actions/github', () => ({
default: githubMock
}));
jest.unstable_mockModule('node-fetch', () => ({
default: fetchMock
}));
describe('manual dispatch integration', () => {
test('run() posts a Discord embed using manual workflow inputs', async () => {
const { run } = await import('../index.js');
await run();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock.mock.calls[0][0]).toBe('https://discord.com/api/webhooks/test/webhook?wait=true');
const request = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(request).toEqual({
embeds: [{
title: 'v1.2.3',
url: 'https://github.com/owner/repo/releases/tag/v1.2.3',
color: '2105893',
description: '## Changes\n- Added manual dispatch support\n- Verified webhook payload',
footer: {}
}],
username: 'Release Changelog',
avatar_url: 'https://example.com/avatar.png',
content: '||@everyone||'
});
expect(coreMock.setFailed).not.toHaveBeenCalled();
expect(coreMock.info).toHaveBeenCalledWith('{"id":"discord-message-id"}');
});
});