mirror of
https://github.com/SethCohen/github-releases-to-discord
synced 2026-09-14 20:13:23 +00:00
feat: add manual dispatch support for GitHub Actions to test Discord webhook integration with optional release inputs. resolves #52
This commit is contained in:
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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"}');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user