* feat: support yml or yaml (#427)

test: support yml or yaml

chore: lint

* Fix getting permissions for the contributors who are a part of the team (#424)

* Fix getActorPermission to handle team-based permissions

* Use 'user.permissions' object to return permission

* Add RepoPermission type

* Make new build

* feat: update runtime to node 24

* fix: eslint-plugin dep

* fix: remove unused type

* docs: update whats new

---------

Co-authored-by: Patrick Lee Scott <[email protected]>
Co-authored-by: Volodymyr Zotov <[email protected]>
This commit is contained in:
Peter Evans
2025-11-24 13:13:10 +00:00
committed by GitHub
co-authored by Patrick Lee Scott Volodymyr Zotov
parent 9a73d036b8
commit e1b4e266bc
17 changed files with 656 additions and 618 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: 20.x
node-version-file: package.json
cache: npm
- run: npm ci
- run: npm run build
+30
View File
@@ -0,0 +1,30 @@
name: Hello yaml Command
on:
repository_dispatch:
types: [hello-yaml-local-command]
jobs:
helloYaml:
runs-on: ubuntu-latest
steps:
- name: Add reaction
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
reactions: hooray
- name: Create URL to the run output
id: vars
run: echo "run-url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_OUTPUT
- name: Create comment
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.client_payload.github.payload.issue.number }}
body: |
Hello @${{ github.event.client_payload.github.actor }}!
This command was in a workflow file with the `.yaml` extension.
[Click here to see the command run output][1]
[1]: ${{ steps.vars.outputs.run-url }}
@@ -19,6 +19,7 @@ jobs:
with:
token: ${{ secrets.REPO_ACCESS_TOKEN }}
commands: |
hello-yaml-local
hello-world-local
ping-local
permission: none
+1 -1
View File
@@ -11,7 +11,7 @@ on:
type: choice
description: The major version tag to update
options:
- v4
- v5
jobs:
tag:
+5 -5
View File
@@ -54,7 +54,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v4
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
commands: |
@@ -102,7 +102,7 @@ You can use a [PAT](https://docs.github.com/en/github/authenticating-to-github/c
```yml
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v4
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
reaction-token: ${{ secrets.PAT }}
@@ -178,7 +178,7 @@ It will also contain any static arguments if configured.
To demonstrate, take the following configuration as an example.
```yml
- uses: peter-evans/slash-command-dispatch@v4
- uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
commands: |
@@ -248,7 +248,7 @@ The simplest response is to add a :tada: reaction to the comment.
```yml
- name: Add reaction
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -264,7 +264,7 @@ Another option is to reply with a new comment containing a link to the run outpu
run: echo "run-url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_OUTPUT
- name: Create comment
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
+1 -1
View File
@@ -41,7 +41,7 @@ outputs:
error-message:
description: 'Validation errors when using `workflow` dispatch.'
runs:
using: 'node20'
using: 'node24'
main: 'dist/index.js'
branding:
icon: 'target'
+50 -18
View File
@@ -293,24 +293,41 @@ class GitHubHelper {
};
}
getActorPermission(repo, actor) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
// https://docs.github.com/en/graphql/reference/enums#repositorypermission
// https://docs.github.com/en/graphql/reference/objects#repositorycollaboratoredge
// Returns 'READ', 'TRIAGE', 'WRITE', 'MAINTAIN', 'ADMIN'
const query = `query CollaboratorPermission($owner: String!, $repo: String!, $collaborator: String) {
repository(owner:$owner, name:$repo) {
collaborators(login: $collaborator) {
edges {
permission
}
}
}
}`;
const collaboratorPermission = yield this.octokit.graphql(query, Object.assign(Object.assign({}, repo), { collaborator: actor }));
core.debug(`CollaboratorPermission: ${(0, util_1.inspect)(collaboratorPermission.repository.collaborators.edges)}`);
return collaboratorPermission.repository.collaborators.edges.length > 0
? collaboratorPermission.repository.collaborators.edges[0].permission.toLowerCase()
: 'none';
// Use the REST API approach which can detect both direct and team-based permissions
// This is more reliable than the GraphQL approach for team permissions and works better with default GITHUB_TOKEN
try {
const { data: collaboratorPermission } = yield this.octokit.rest.repos.getCollaboratorPermissionLevel(Object.assign(Object.assign({}, repo), { username: actor }));
const permissions = (_a = collaboratorPermission.user) === null || _a === void 0 ? void 0 : _a.permissions;
core.debug(`REST API collaborator permission: ${(0, util_1.inspect)(permissions)}`);
// Use the detailed permissions object to get the highest permission level
if (permissions) {
// Check permissions in order of highest to lowest
if (permissions.admin) {
return 'admin';
}
else if (permissions.maintain) {
return 'maintain';
}
else if (permissions.push) {
return 'write';
}
else if (permissions.triage) {
core.debug(`User ${actor} has triage permission via REST API`);
return 'triage';
}
else if (permissions.pull) {
core.debug(`User ${actor} has read permission via REST API`);
return 'read';
}
}
return 'none';
}
catch (error) {
core.debug(`REST API permission check failed: ${utils.getErrorMessage(error)}`);
return 'none';
}
});
}
tryAddReaction(repo, commentId, reaction) {
@@ -350,7 +367,8 @@ class GitHubHelper {
}
createWorkflowDispatch(cmd, clientPayload) {
return __awaiter(this, void 0, void 0, function* () {
const workflow = `${cmd.command}${cmd.event_type_suffix}.yml`;
const workflowName = `${cmd.command}${cmd.event_type_suffix}`;
const workflow = yield this.getWorkflow(cmd.repository, workflowName);
const slashCommand = clientPayload.slash_command;
const ref = slashCommand.args.named.ref
? slashCommand.args.named.ref
@@ -370,6 +388,20 @@ class GitHubHelper {
core.info(`Command '${cmd.command}' dispatched to workflow '${workflow}' in '${cmd.repository}'`);
});
}
getWorkflow(repository, workflowName) {
return __awaiter(this, void 0, void 0, function* () {
core.debug(`Getting workflow ${workflowName} for repository ${repository}`);
const { data: workflows } = yield this.octokit.rest.actions.listRepoWorkflows(Object.assign({}, this.parseRepository(repository)));
for (const workflow of workflows.workflows) {
if (workflow.path === `${workflowName}.yml` ||
workflow.path === `${workflowName}.yaml`) {
core.debug(`Selecting workflow file ${workflow.path}`);
return workflow.path;
}
}
throw new Error(`Workflow ${workflowName} not found`);
});
}
getDefaultBranch(repository) {
return __awaiter(this, void 0, void 0, function* () {
const { data: repo } = yield this.octokit.rest.repos.get(Object.assign({}, this.parseRepository(repository)));
+3 -3
View File
@@ -8,7 +8,7 @@ For example, the following basic configuration means that all commands must have
```yml
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v4
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
commands: |
@@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v4
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
config: >
@@ -84,7 +84,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v4
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
config-from-file: .github/slash-command-dispatch.json
+7 -7
View File
@@ -50,7 +50,7 @@ jobs:
# Add reaction to the comment
- name: Add reaction
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -111,7 +111,7 @@ jobs:
# Add reaction to the comment
- name: Add reaction
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -158,7 +158,7 @@ jobs:
git push
- name: Add reaction
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -204,7 +204,7 @@ jobs:
git push --force-with-lease
- name: Update comment
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -218,7 +218,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Update comment
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -279,7 +279,7 @@ jobs:
git push
- name: Add reaction
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -301,7 +301,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Update comment
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
+2 -2
View File
@@ -19,7 +19,7 @@ Follow this guide to get started with a working `/example` command.
runs-on: ubuntu-latest
steps:
- name: Add reaction
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -56,7 +56,7 @@ Command processing setup is complete! Now we need to setup command dispatch for
runs-on: ubuntu-latest
steps:
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v4
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
commands: example
+12
View File
@@ -1,3 +1,15 @@
## Updating from `v4` to `v5`
### Breaking changes
- If using self-hosted runners or GitHub Enterprise Server, there are minimum requirements for `v5` to run. See "What's new" below for details.
### What's new
- Updated runtime to Node.js 24
- The action now requires a minimum version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) for the Actions runner. Update self-hosted runners to v2.327.1 or later to ensure compatibility.
- The `.yaml` extension is now supported for dispatch type `workflow`.
- A change has been made to fetching permissions for contributors. Hopefully this will resolve some issues with fetching permissions for contributors that are part of a team.
## Updating from `v3` to `v4`
### Breaking changes
+5 -5
View File
@@ -19,7 +19,7 @@ There are significant differences in the action's behaviour when using `workflow
It is important to name the `workflow_dispatch` event workflow correctly since the action targets the workflow based on its filename.
The target filename is a combination of the command name and the `event-type-suffix`.
Additionally, the file extension must be `.yml`.
The file extensions `.yml` and `.yaml` are supported.
For the following example configuration, the target workflows are:
- `deploy-command.yml`
@@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v4
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
commands: |
@@ -85,7 +85,7 @@ The simplest response is to add a :tada: reaction to the comment.
```yml
- name: Add reaction
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.PAT }}
repository: ${{ github.event.inputs.repository }}
@@ -107,7 +107,7 @@ The `error-message` output can be used to provide feedback to the user as follow
```yml
- name: Slash Command Dispatch
id: scd
uses: peter-evans/slash-command-dispatch@v4
uses: peter-evans/slash-command-dispatch@v5
with:
token: ${{ secrets.PAT }}
commands: |
@@ -118,7 +118,7 @@ The `error-message` output can be used to provide feedback to the user as follow
- name: Edit comment with error message
if: steps.scd.outputs.error-message
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ github.event.comment.id }}
body: |
+461 -536
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -1,9 +1,12 @@
{
"name": "slash-command-dispatch",
"version": "4.0.0",
"version": "5.0.0",
"private": true,
"description": "Facilitates 'ChatOps' by creating dispatch events for slash commands",
"main": "lib/main.js",
"engines": {
"node": ">=24.4.0"
},
"scripts": {
"build": "tsc && ncc build",
"format": "prettier --write '**/*.ts'",
@@ -39,6 +42,7 @@
"devDependencies": {
"@types/jest": "^27.0.3",
"@types/node": "^16.18.126",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vercel/ncc": "^0.38.4",
"eslint": "^8.57.1",
+1 -1
View File
@@ -175,7 +175,7 @@ export function configIsValid(config: Command[]): string | null {
}
export function actorHasPermission(
actorPermission: string,
actorPermission: utils.RepoPermission,
commandPermission: string
): boolean {
const permissionLevels = Object.freeze({
+63 -37
View File
@@ -25,18 +25,6 @@ interface Repository {
repo: string
}
type CollaboratorPermission = {
repository: {
collaborators: {
edges: [
{
permission: string
}
]
}
}
}
export class GitHubHelper {
private octokit: InstanceType<typeof Octokit>
@@ -55,32 +43,47 @@ export class GitHubHelper {
}
}
async getActorPermission(repo: Repository, actor: string): Promise<string> {
// https://docs.github.com/en/graphql/reference/enums#repositorypermission
// https://docs.github.com/en/graphql/reference/objects#repositorycollaboratoredge
// Returns 'READ', 'TRIAGE', 'WRITE', 'MAINTAIN', 'ADMIN'
const query = `query CollaboratorPermission($owner: String!, $repo: String!, $collaborator: String) {
repository(owner:$owner, name:$repo) {
collaborators(login: $collaborator) {
edges {
permission
}
async getActorPermission(
repo: Repository,
actor: string
): Promise<utils.RepoPermission> {
// Use the REST API approach which can detect both direct and team-based permissions
// This is more reliable than the GraphQL approach for team permissions and works better with default GITHUB_TOKEN
try {
const {data: collaboratorPermission} =
await this.octokit.rest.repos.getCollaboratorPermissionLevel({
...repo,
username: actor
})
const permissions = collaboratorPermission.user?.permissions
core.debug(`REST API collaborator permission: ${inspect(permissions)}`)
// Use the detailed permissions object to get the highest permission level
if (permissions) {
// Check permissions in order of highest to lowest
if (permissions.admin) {
return 'admin'
} else if (permissions.maintain) {
return 'maintain'
} else if (permissions.push) {
return 'write'
} else if (permissions.triage) {
core.debug(`User ${actor} has triage permission via REST API`)
return 'triage'
} else if (permissions.pull) {
core.debug(`User ${actor} has read permission via REST API`)
return 'read'
}
}
}`
const collaboratorPermission =
await this.octokit.graphql<CollaboratorPermission>(query, {
...repo,
collaborator: actor
})
core.debug(
`CollaboratorPermission: ${inspect(
collaboratorPermission.repository.collaborators.edges
)}`
)
return collaboratorPermission.repository.collaborators.edges.length > 0
? collaboratorPermission.repository.collaborators.edges[0].permission.toLowerCase()
: 'none'
return 'none'
} catch (error) {
core.debug(
`REST API permission check failed: ${utils.getErrorMessage(error)}`
)
return 'none'
}
}
async tryAddReaction(
@@ -150,7 +153,8 @@ export class GitHubHelper {
cmd: Command,
clientPayload: ClientPayload
): Promise<void> {
const workflow = `${cmd.command}${cmd.event_type_suffix}.yml`
const workflowName = `${cmd.command}${cmd.event_type_suffix}`
const workflow = await this.getWorkflow(cmd.repository, workflowName)
const slashCommand: SlashCommandPayload = clientPayload.slash_command
const ref = slashCommand.args.named.ref
? slashCommand.args.named.ref
@@ -181,6 +185,28 @@ export class GitHubHelper {
)
}
private async getWorkflow(
repository: string,
workflowName: string
): Promise<string> {
core.debug(`Getting workflow ${workflowName} for repository ${repository}`)
const {data: workflows} = await this.octokit.rest.actions.listRepoWorkflows(
{
...this.parseRepository(repository)
}
)
for (const workflow of workflows.workflows) {
if (
workflow.path === `${workflowName}.yml` ||
workflow.path === `${workflowName}.yaml`
) {
core.debug(`Selecting workflow file ${workflow.path}`)
return workflow.path
}
}
throw new Error(`Workflow ${workflowName} not found`)
}
private async getDefaultBranch(repository: string): Promise<string> {
const {data: repo} = await this.octokit.rest.repos.get({
...this.parseRepository(repository)
+8
View File
@@ -1,5 +1,13 @@
import * as core from '@actions/core'
export type RepoPermission =
| 'admin'
| 'maintain'
| 'write'
| 'triage'
| 'read'
| 'none'
export function getInputAsArray(
name: string,
options?: core.InputOptions