Compare commits

...

55 commits
v2.2.0 ... main

Author SHA1 Message Date
dependabot[bot]
dd126dd8c2
Bump postcss from 8.4.23 to 8.4.33 (#115)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.23 to 8.4.33.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.23...8.4.33)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 14:00:08 -05:00
dependabot[bot]
30602c8f56
Bump vite from 4.3.3 to 4.3.9 (#102)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.3 to 4.3.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.3.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 13:50:41 -05:00
dependabot[bot]
f24a409f6b
Bump word-wrap from 1.2.3 to 1.2.4 (#105)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 13:50:28 -05:00
Michael Shick
b8f338c590
2.8.2 2024-02-01 13:43:55 -05:00
Robbie Ostrow
74e66d7778
Bump runtime to node20 from node16 (#114)
https://github.blog/changelog/2023-09-22-github-actions-transitioning-from-node-16-to-node-20/
2024-02-01 13:42:24 -05:00
Michael Shick
8fedd701c5
bumping package to node 20 2024-02-01 13:40:56 -05:00
Michael Shick
7c0890544f
2.8.1 2023-05-12 14:10:09 -04:00
Michael Shick
12282e9c93
Update action.yml to add find and replace 2023-05-12 14:09:24 -04:00
Michael Shick
918f138773
2.8.0 2023-05-07 09:04:31 -04:00
Michael Shick
84c8c4f13e
new build 2023-05-07 09:04:04 -04:00
Michael Shick
445c144052
clean up 2023-05-07 09:03:54 -04:00
Michael Shick
ff82b38f95
update replace behavior 2023-05-07 09:03:15 -04:00
Michael Shick
25e7c93662
2.7.0 2023-05-07 08:52:43 -04:00
Michael Shick
09331f990d
new build 2023-05-07 08:52:22 -04:00
Michael Shick
06b07c2e70
try to get release building 2023-05-07 08:51:49 -04:00
Michael Shick
f8c324a9fc
find-and-replace functionality (#100)
* find-and-replace functionality
2023-05-07 08:50:52 -04:00
allcontributors[bot]
5cc4621415
docs: add twang817 as a contributor for code (#98)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-05-05 16:12:15 -04:00
Michael Shick
a624db19fc
2.6.1 2023-05-05 16:09:45 -04:00
Michael Shick
475aa8ac2b
new build 2023-05-05 16:09:23 -04:00
Michael Shick
a8a22ad616
fix message fallback behavior 2023-05-05 16:09:03 -04:00
Michael Shick
9367dbbf15
2.6.0 2023-05-05 16:05:32 -04:00
Michael Shick
445bbc6324
new build 2023-05-05 16:05:09 -04:00
Michael Shick
a251f051d3
Preformatted messages (#97)
* refactor message creation
2023-05-05 16:02:36 -04:00
Michael Shick
ef723874d4
2.5.1 2023-05-05 15:48:04 -04:00
Michael Shick
f593e19b25
new build 2023-05-05 15:47:43 -04:00
Michael Shick
3db21c2292
messagePath is not an array 2023-05-05 15:47:28 -04:00
Michael Shick
7f44ca3b15
Fix codeblock 2023-05-04 17:37:33 -04:00
Michael Shick
d0b6b97ab0
2.5.0 2023-05-04 17:33:20 -04:00
Michael Shick
73ffb32342
new build 2023-05-04 17:32:54 -04:00
Michael Shick
4a541a260f
Multiline message-path and concatenation (#88) 2023-05-04 17:25:26 -04:00
Alex Hatzenbuhler
a0c6c0cbf4
docs: Add default of false to update-only (#96)
When I changed this to a boolean internally I forgot to reflect the `false` default on the README. this PR fixes it.
2023-05-04 13:50:05 -04:00
Michael Shick
75ab356ce1
Merge branch 'main' of https://github.com/mshick/add-pr-comment 2023-05-03 10:26:39 -04:00
Michael Shick
bafc97d9ce
add npm hook to build 2023-05-03 10:26:36 -04:00
allcontributors[bot]
442e4f9c93
docs: add ahatzz11 as a contributor for code (#94)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-05-02 20:45:06 -04:00
Michael Shick
3a15c7386b
2.4.0 2023-05-02 18:20:57 -04:00
Michael Shick
f0a003891a
new build 2023-05-02 18:20:12 -04:00
Alex Hatzenbuhler
1dff58b1a3
Add update-only configuration option (#92) 2023-05-02 18:11:22 -04:00
Michael Shick
387ece43e3
target doesn't work 2023-04-24 21:38:16 -04:00
Michael Shick
84295c55d2
fix ci event 2023-04-24 21:29:40 -04:00
Michael Shick
99718eaf16
set pr types 2023-04-24 10:40:46 -04:00
Michael Shick
a02677ce9e
use pull_request_target instead 2023-04-24 10:30:33 -04:00
allcontributors[bot]
655ef16eab
docs: add ahanoff as a contributor for code (#86)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-04-24 08:56:34 -04:00
allcontributors[bot]
484efd8915
docs: add vincent-joignie-dd as a contributor for code (#85)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-04-24 08:54:41 -04:00
allcontributors[bot]
1ea82c5459
docs: add aryella-lacerda as a contributor for code (#84)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-04-24 08:53:50 -04:00
allcontributors[bot]
378906227c
docs: add ReenigneArcher as a contributor for code (#83)
* docs: update README.md [skip ci]

* docs: create .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-04-24 08:52:21 -04:00
Michael Shick
71ddf4a572
2.3.0 2023-04-24 08:23:13 -04:00
Michael Shick
0baab3bf91
clean up message-path test fixtures 2023-04-24 08:22:38 -04:00
Michael Shick
a5ba281881
Merge branch 'main' of https://github.com/mshick/add-pr-comment 2023-04-24 08:17:41 -04:00
Michael Shick
0e8dc756bc
update tests 2023-04-24 08:16:55 -04:00
ReenigneArcher
1605572889
add custom owner and repo inputs (#78)
* add custom owner and repo inputs

* add test for comment in another repo
2023-04-24 08:14:29 -04:00
Michael Shick
7ca8398d28
set action inputs to required if they have defaults 2023-04-23 09:13:45 -04:00
Michael Shick
9c1e156588
clone inputs 2023-04-23 08:57:06 -04:00
Michael Shick
7ca909c028
reset more inputs 2023-04-23 08:54:06 -04:00
Michael Shick
747b5a722c
2.2.1 2023-04-22 07:55:54 -04:00
Michael Shick
9412131e1e
bump deps 2023-04-22 07:55:22 -04:00
31 changed files with 7939 additions and 10936 deletions

70
.all-contributorsrc Normal file
View file

@ -0,0 +1,70 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitConvention": "angular",
"contributors": [
{
"login": "ReenigneArcher",
"name": "ReenigneArcher",
"avatar_url": "https://avatars.githubusercontent.com/u/42013603?v=4",
"profile": "https://app.lizardbyte.dev",
"contributions": [
"code"
]
},
{
"login": "aryella-lacerda",
"name": "Aryella Lacerda",
"avatar_url": "https://avatars.githubusercontent.com/u/28730324?v=4",
"profile": "https://github.com/aryella-lacerda",
"contributions": [
"code"
]
},
{
"login": "vincent-joignie-dd",
"name": "vincent-joignie-dd",
"avatar_url": "https://avatars.githubusercontent.com/u/103102299?v=4",
"profile": "https://github.com/vincent-joignie-dd",
"contributions": [
"code"
]
},
{
"login": "ahanoff",
"name": "Akhan Zhakiyanov",
"avatar_url": "https://avatars.githubusercontent.com/u/2371703?v=4",
"profile": "https://ahanoff.dev",
"contributions": [
"code"
]
},
{
"login": "ahatzz11",
"name": "Alex Hatzenbuhler",
"avatar_url": "https://avatars.githubusercontent.com/u/6256032?v=4",
"profile": "https://github.com/ahatzz11",
"contributions": [
"code"
]
},
{
"login": "twang817",
"name": "Tommy Wang",
"avatar_url": "https://avatars.githubusercontent.com/u/766820?v=4",
"profile": "http://www.august8.net",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "add-pr-comment",
"projectOwner": "mshick"
}

1
.github/test/file-1.txt vendored Normal file
View file

@ -0,0 +1 @@
Hello

1
.github/test/file-2.txt vendored Normal file
View file

@ -0,0 +1 @@
Goodbye

BIN
.github/test/image.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -2,6 +2,10 @@ name: ci
on: on:
pull_request: pull_request:
types:
- opened
- synchronize
- reopened
jobs: jobs:
test: test:
@ -58,7 +62,25 @@ jobs:
- uses: ./ - uses: ./
with: with:
preformatted: true
message-id: path
message-path: |
.github/test/file-*.txt
- uses: ./
with:
message-id: text
message: | message: |
**Hello ${{ github.run_number }}** **Hello**
🌏 🌏
! !
- uses: ./
with:
message-id: text
find: |
Hello
🌏
replace: |
Goodnight
🌕

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ node_modules/
# Editors # Editors
.vscode .vscode
.idea/**
# Logs # Logs
logs logs

View file

@ -1 +1 @@
v16 v20

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 Michael Shick Copyright (c) 2024 Michael Shick
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

198
README.md
View file

@ -1,5 +1,11 @@
# add-pr-comment # add-pr-comment
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
A GitHub Action which adds a comment to a pull request's issue. A GitHub Action which adds a comment to a pull request's issue.
This actions also works on [issue](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues), This actions also works on [issue](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues),
@ -63,21 +69,27 @@ jobs:
## Configuration options ## Configuration options
| Input | Location | Description | Required | Default | | Input | Location | Description | Required | Default |
| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------- | -------- | ------------------ | | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------- |
| message | with | The message you'd like displayed, supports Markdown and all valid Unicode characters. | maybe | | | message | with | The message you'd like displayed, supports Markdown and all valid Unicode characters. | maybe | |
| message-path | with | Path to a message you'd like displayed. Will be read and displayed just like a normal message. | maybe | | | message-path | with | Path to a message you'd like displayed. Will be read and displayed just like a normal message. Supports multi-line input and globs. Multiple messages will be concatenated. | maybe | |
| message-success | with | A message override, printed in case of success. | no | | | message-success | with | A message override, printed in case of success. | no | |
| message-failure | with | A message override, printed in case of failure. | no | | | message-failure | with | A message override, printed in case of failure. | no | |
| message-cancelled | with | A message override, printed in case of cancelled. | no | | | message-cancelled | with | A message override, printed in case of cancelled. | no | |
| message-skipped | with | A message override, printed in case of skipped. | no | | | message-skipped | with | A message override, printed in case of skipped. | no | |
| status | with | Required if you want to use message status overrides. | no | {{ job.status }} | | status | with | Required if you want to use message status overrides. | no | {{ job.status }} |
| repo-owner | with | Owner of the repo. | no | {{ github.repository_owner }} |
| repo-name | with | Name of the repo. | no | {{ github.event.repository.name }} |
| repo-token | with | Valid GitHub token, either the temporary token GitHub provides or a personal access token. | no | {{ github.token }} | | repo-token | with | Valid GitHub token, either the temporary token GitHub provides or a personal access token. | no | {{ github.token }} |
| message-id | with | Message id to use when searching existing comments. If found, updates the existing (sticky comment). | no | | | message-id | with | Message id to use when searching existing comments. If found, updates the existing (sticky comment). | no | |
| refresh-message-position | with | Should the sticky message be the last one in the PR's feed. | no | false | | refresh-message-position | with | Should the sticky message be the last one in the PR's feed. | no | false |
| allow-repeats | with | Boolean flag to allow identical messages to be posted each time this action is run. | no | false | | allow-repeats | with | Boolean flag to allow identical messages to be posted each time this action is run. | no | false |
| proxy-url | with | String for your proxy service URL if you'd like this to work with fork-based PRs. | no | | | proxy-url | with | String for your proxy service URL if you'd like this to work with fork-based PRs. | no | |
| issue | with | Optional issue number override. | no | | | issue | with | Optional issue number override. | no | |
| update-only | with | Only update the comment if it already exists. | no | false |
| GITHUB_TOKEN | env | Valid GitHub token, can alternatively be defined in the env. | no | | | GITHUB_TOKEN | env | Valid GitHub token, can alternatively be defined in the env. | no | |
| preformatted | with | Treat message text as pre-formatted and place it in a codeblock | no | |
| find | with | Patterns to find in an existing message and replace with either `replace` text or a resolved `message`. See [Find-and-Replace](#find-and-replace) for more detail. | no | |
| replace | with | Strings to replace a found pattern with. Each new line is a new replacement, or if you only have one pattern, you can replace with a multiline string. | no | |
## Advanced Uses ## Advanced Uses
@ -135,6 +147,161 @@ jobs:
Uh oh! Uh oh!
``` ```
### Multiple Message Files
Instead of directly setting the message you can also load a file with the text
of your message using `message-path`. `message-path` supports loading multiple
files and files on multiple lines, the contents of which will be concatenated.
**Example**
```yaml
on:
pull_request:
jobs:
pr:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: mshick/add-pr-comment@v2
if: always()
with:
message-path: |
message-part-*.txt
```
### Find-and-Replace
Patterns can be matched and replaced to update comments. This could be useful
for some situations, for instance, updating a checklist comment.
Find is a regular expression passed to the [RegExp() constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp). You can also
include modifiers to override the default `gi`.
**Example**
Original message:
```
[ ] Hello
[ ] World
```
Action:
```yaml
on:
pull_request:
jobs:
pr:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: mshick/add-pr-comment@v2
if: always()
with:
find: |
\n\\[ \\]
replace: |
[X]
```
Final message:
```
[X] Hello
[X] World
```
Multiple find and replaces can be used:
**Example**
Original message:
```
hello world!
```
Action:
```yaml
on:
pull_request:
jobs:
pr:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: mshick/add-pr-comment@v2
if: always()
with:
find: |
hello
world
replace: |
goodnight
moon
```
Final message:
```
goodnight moon!
```
It defaults to your resolved message (either from `message` or `message-path`) to
do a replacement:
**Example**
Original message:
```
hello
<< FILE_CONTENTS >>
world
```
Action:
```yaml
on:
pull_request:
jobs:
pr:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: mshick/add-pr-comment@v2
if: always()
with:
message-path: |
message.txt
find: |
<< FILE_CONTENTS >>
```
Final message:
```
hello
secret message from message.txt
world
```
### Bring your own issues ### Bring your own issues
You can set an issue id explicitly. Helpful for cases where you want to post You can set an issue id explicitly. Helpful for cases where you want to post
@ -168,3 +335,30 @@ jobs:
message: | message: |
**Howdie!** **Howdie!**
``` ```
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://app.lizardbyte.dev"><img src="https://avatars.githubusercontent.com/u/42013603?v=4?s=100" width="100px;" alt="ReenigneArcher"/><br /><sub><b>ReenigneArcher</b></sub></a><br /><a href="https://github.com/mshick/add-pr-comment/commits?author=ReenigneArcher" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aryella-lacerda"><img src="https://avatars.githubusercontent.com/u/28730324?v=4?s=100" width="100px;" alt="Aryella Lacerda"/><br /><sub><b>Aryella Lacerda</b></sub></a><br /><a href="https://github.com/mshick/add-pr-comment/commits?author=aryella-lacerda" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vincent-joignie-dd"><img src="https://avatars.githubusercontent.com/u/103102299?v=4?s=100" width="100px;" alt="vincent-joignie-dd"/><br /><sub><b>vincent-joignie-dd</b></sub></a><br /><a href="https://github.com/mshick/add-pr-comment/commits?author=vincent-joignie-dd" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ahanoff.dev"><img src="https://avatars.githubusercontent.com/u/2371703?v=4?s=100" width="100px;" alt="Akhan Zhakiyanov"/><br /><sub><b>Akhan Zhakiyanov</b></sub></a><br /><a href="https://github.com/mshick/add-pr-comment/commits?author=ahanoff" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ahatzz11"><img src="https://avatars.githubusercontent.com/u/6256032?v=4?s=100" width="100px;" alt="Alex Hatzenbuhler"/><br /><sub><b>Alex Hatzenbuhler</b></sub></a><br /><a href="https://github.com/mshick/add-pr-comment/commits?author=ahatzz11" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.august8.net"><img src="https://avatars.githubusercontent.com/u/766820?v=4?s=100" width="100px;" alt="Tommy Wang"/><br /><sub><b>Tommy Wang</b></sub></a><br /><a href="https://github.com/mshick/add-pr-comment/commits?author=twang817" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View file

@ -3,12 +3,16 @@ import * as github from '@actions/github'
import { WebhookPayload } from '@actions/github/lib/interfaces' import { WebhookPayload } from '@actions/github/lib/interfaces'
import { rest } from 'msw' import { rest } from 'msw'
import { setupServer } from 'msw/node' import { setupServer } from 'msw/node'
import * as fs from 'node:fs/promises'
import * as path from 'node:path' import * as path from 'node:path'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import run from '../src/main' import run from '../src/main'
import apiResponse from './sample-pulls-api-response.json' import apiResponse from './sample-pulls-api-response.json'
const repoFullName = 'foo/bar' const messagePath1Fixture = path.resolve(__dirname, './message-part-1.txt')
const messagePath1FixturePayload = await fs.readFile(messagePath1Fixture, 'utf-8')
const messagePath2Fixture = path.resolve(__dirname, './message-part-2.txt')
const repoToken = '12345' const repoToken = '12345'
const commitSha = 'abc123' const commitSha = 'abc123'
const simpleMessage = 'hello world' const simpleMessage = 'hello world'
@ -16,25 +20,36 @@ const simpleMessage = 'hello world'
type Inputs = { type Inputs = {
message: string | undefined message: string | undefined
'message-path': string | undefined 'message-path': string | undefined
'repo-owner': string
'repo-name': string
'repo-token': string 'repo-token': string
'message-id': string 'message-id': string
'allow-repeats': string 'allow-repeats': string
'message-pattern'?: string
'message-success'?: string 'message-success'?: string
'message-failure'?: string 'message-failure'?: string
'message-cancelled'?: string 'message-cancelled'?: string
'message-skipped'?: string 'message-skipped'?: string
'update-only'?: string
preformatted?: string
status?: 'success' | 'failure' | 'cancelled' | 'skipped' status?: 'success' | 'failure' | 'cancelled' | 'skipped'
} }
const inputs: Inputs = { const defaultInputs: Inputs = {
message: '', message: '',
'message-path': undefined, 'message-path': undefined,
'repo-token': '', 'repo-owner': 'foo',
'repo-name': 'bar',
'repo-token': repoToken,
'message-id': 'add-pr-comment', 'message-id': 'add-pr-comment',
'allow-repeats': 'false', 'allow-repeats': 'false',
status: 'success',
} }
let issueNumber = 1 const defaultIssueNumber = 1
let inputs = defaultInputs
let issueNumber = defaultIssueNumber
let getCommitPullsResponse let getCommitPullsResponse
let getIssueCommentsResponse let getIssueCommentsResponse
let postIssueCommentsResponse = { let postIssueCommentsResponse = {
@ -50,29 +65,29 @@ let messagePayload: MessagePayload | undefined
vi.mock('@actions/core') vi.mock('@actions/core')
export const handlers = [ const handlers = [
rest.post( rest.post(
`https://api.github.com/repos/${repoFullName}/issues/:issueNumber/comments`, `https://api.github.com/repos/:repoUser/:repoName/issues/:issueNumber/comments`,
async (req, res, ctx) => { async (req, res, ctx) => {
messagePayload = await req.json<MessagePayload>() messagePayload = await req.json<MessagePayload>()
return res(ctx.status(200), ctx.json(postIssueCommentsResponse)) return res(ctx.status(200), ctx.json(postIssueCommentsResponse))
}, },
), ),
rest.patch( rest.patch(
`https://api.github.com/repos/${repoFullName}/issues/comments/:commentId`, `https://api.github.com/repos/:repoUser/:repoName/issues/comments/:commentId`,
async (req, res, ctx) => { async (req, res, ctx) => {
messagePayload = await req.json<MessagePayload>() messagePayload = await req.json<MessagePayload>()
return res(ctx.status(200), ctx.json(postIssueCommentsResponse)) return res(ctx.status(200), ctx.json(postIssueCommentsResponse))
}, },
), ),
rest.get( rest.get(
`https://api.github.com/repos/${repoFullName}/issues/:issueNumber/comments`, `https://api.github.com/repos/:repoUser/:repoName/issues/:issueNumber/comments`,
(req, res, ctx) => { (req, res, ctx) => {
return res(ctx.status(200), ctx.json(getIssueCommentsResponse)) return res(ctx.status(200), ctx.json(getIssueCommentsResponse))
}, },
), ),
rest.get( rest.get(
`https://api.github.com/repos/${repoFullName}/commits/:commitSha/pulls`, `https://api.github.com/repos/:repoUser/:repoName/commits/:commitSha/pulls`,
(req, res, ctx) => { (req, res, ctx) => {
return res(ctx.status(200), ctx.json(getCommitPullsResponse)) return res(ctx.status(200), ctx.json(getCommitPullsResponse))
}, },
@ -81,12 +96,17 @@ export const handlers = [
const server = setupServer(...handlers) const server = setupServer(...handlers)
describe('add-pr-comment action', () => { beforeAll(() => {
beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) vi.spyOn(console, 'log').mockImplementation(() => {})
server.listen({ onUnhandledRequest: 'error' })
})
afterAll(() => server.close()) afterAll(() => server.close())
beforeEach(() => { beforeEach(() => {
issueNumber = 1 inputs = { ...defaultInputs }
issueNumber = defaultIssueNumber
messagePayload = undefined
vi.resetModules() vi.resetModules()
github.context.sha = commitSha github.context.sha = commitSha
@ -97,7 +117,7 @@ describe('add-pr-comment action', () => {
number: issueNumber, number: issueNumber,
}, },
repository: { repository: {
full_name: repoFullName, full_name: `${inputs['repo-owner']}/${inputs['repo-name']}`,
name: 'bar', name: 'bar',
owner: { owner: {
login: 'bar', login: 'bar',
@ -111,7 +131,7 @@ describe('add-pr-comment action', () => {
server.resetHandlers() server.resetHandlers()
}) })
vi.mocked(core.getInput).mockImplementation((name: string, options?: core.InputOptions) => { const getInput = (name: string, options?: core.InputOptions) => {
const value = inputs[name] ?? '' const value = inputs[name] ?? ''
if (options?.required && value === undefined) { if (options?.required && value === undefined) {
@ -119,11 +139,39 @@ describe('add-pr-comment action', () => {
} }
return value return value
}) }
function getMultilineInput(name, options) {
const inputs = getInput(name, options)
.split('\n')
.filter((x) => x !== '')
if (options && options.trimWhitespace === false) {
return inputs
}
return inputs.map((input) => input.trim())
}
function getBooleanInput(name, options) {
const trueValue = ['true', 'True', 'TRUE']
const falseValue = ['false', 'False', 'FALSE']
const val = getInput(name, options)
if (trueValue.includes(val)) return true
if (falseValue.includes(val)) return false
throw new TypeError(
`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` +
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``,
)
}
vi.mocked(core.getInput).mockImplementation(getInput)
vi.mocked(core.getMultilineInput).mockImplementation(getMultilineInput)
vi.mocked(core.getBooleanInput).mockImplementation(getBooleanInput)
describe('add-pr-comment action', () => {
it('creates a comment with message text', async () => { it('creates a comment with message text', async () => {
inputs.message = simpleMessage inputs.message = simpleMessage
inputs['repo-token'] = repoToken
inputs['allow-repeats'] = 'true' inputs['allow-repeats'] = 'true'
await expect(run()).resolves.not.toThrow() await expect(run()).resolves.not.toThrow()
@ -133,30 +181,53 @@ describe('add-pr-comment action', () => {
it('creates a comment with a message-path', async () => { it('creates a comment with a message-path', async () => {
inputs.message = undefined inputs.message = undefined
inputs['message-path'] = path.resolve(__dirname, './message.txt') inputs['message-path'] = messagePath1Fixture
inputs['repo-token'] = repoToken
inputs['allow-repeats'] = 'true' inputs['allow-repeats'] = 'true'
await expect(run()).resolves.not.toThrow() await expect(run()).resolves.not.toThrow()
expect(`<!-- add-pr-comment:add-pr-comment -->\n\n${messagePath1FixturePayload}`).toEqual(
messagePayload?.body,
)
expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', postIssueCommentsResponse.id)
})
it('creates a comment with multiple message-paths concatenated', async () => {
inputs.message = undefined
inputs['message-path'] = `${messagePath1Fixture}\n${messagePath2Fixture}`
inputs['allow-repeats'] = 'true'
await expect(run()).resolves.not.toThrow()
expect(
`<!-- add-pr-comment:add-pr-comment -->\n\n${messagePath1FixturePayload}\n${messagePath1FixturePayload}`,
).toEqual(messagePayload?.body)
expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', postIssueCommentsResponse.id)
})
it('supports globs in message paths', async () => {
inputs.message = undefined
inputs['message-path'] = `${path.resolve(__dirname)}/message-part-*.txt`
inputs['allow-repeats'] = 'true'
await expect(run()).resolves.not.toThrow()
expect(
`<!-- add-pr-comment:add-pr-comment -->\n\n${messagePath1FixturePayload}\n${messagePath1FixturePayload}`,
).toEqual(messagePayload?.body)
expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true') expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', postIssueCommentsResponse.id) expect(core.setOutput).toHaveBeenCalledWith('comment-id', postIssueCommentsResponse.id)
}) })
it('fails when both message and message-path are defined', async () => { it('fails when both message and message-path are defined', async () => {
inputs.message = 'foobar' inputs.message = 'foobar'
inputs['message-path'] = path.resolve(__dirname, './message.txt') inputs['message-path'] = messagePath1Fixture
inputs['repo-token'] = repoToken
await expect(run()).resolves.not.toThrow() await expect(run()).resolves.not.toThrow()
expect(core.setFailed).toHaveBeenCalledWith('must specify only one, message or message-path') expect(core.setFailed).toHaveBeenCalledWith('must specify only one, message or message-path')
}) })
it('creates a comment in an existing PR', async () => { it('creates a comment in an existing PR', async () => {
process.env['GITHUB_TOKEN'] = repoToken
inputs.message = simpleMessage inputs.message = simpleMessage
inputs['message-path'] = undefined
inputs['repo-token'] = repoToken
inputs['allow-repeats'] = 'true' inputs['allow-repeats'] = 'true'
github.context.payload = { github.context.payload = {
@ -174,11 +245,41 @@ describe('add-pr-comment action', () => {
expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true') expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true')
}) })
it('does not create a comment when updateOnly is true and no existing comment is found', async () => {
inputs.message = simpleMessage
inputs['allow-repeats'] = 'true'
inputs['update-only'] = 'true'
await expect(run()).resolves.not.toThrow()
expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'false')
})
it('creates a comment in another repo', async () => {
inputs.message = simpleMessage
inputs['repo-owner'] = 'my-owner'
inputs['repo-name'] = 'my-repo'
inputs['allow-repeats'] = 'true'
github.context.payload = {
...github.context.payload,
pull_request: {
number: 0,
},
} as WebhookPayload
issueNumber = apiResponse.result[0].number
getCommitPullsResponse = apiResponse.result
await expect(run()).resolves.not.toThrow()
expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', postIssueCommentsResponse.id)
})
it('safely exits when no issue can be found [using GITHUB_TOKEN in env]', async () => { it('safely exits when no issue can be found [using GITHUB_TOKEN in env]', async () => {
process.env['GITHUB_TOKEN'] = repoToken process.env['GITHUB_TOKEN'] = repoToken
inputs.message = simpleMessage inputs.message = simpleMessage
inputs['message-path'] = undefined
inputs['allow-repeats'] = 'true' inputs['allow-repeats'] = 'true'
github.context.payload = { github.context.payload = {
@ -196,9 +297,7 @@ describe('add-pr-comment action', () => {
it('creates a message when the message id does not exist', async () => { it('creates a message when the message id does not exist', async () => {
inputs.message = simpleMessage inputs.message = simpleMessage
inputs['message-path'] = undefined
inputs['repo-token'] = repoToken
inputs['allow-repeats'] = 'false'
inputs['message-id'] = 'custom-id' inputs['message-id'] = 'custom-id'
const replyBody = [ const replyBody = [
@ -216,9 +315,6 @@ describe('add-pr-comment action', () => {
it('identifies an existing message by id and updates it', async () => { it('identifies an existing message by id and updates it', async () => {
inputs.message = simpleMessage inputs.message = simpleMessage
inputs['message-path'] = undefined
inputs['repo-token'] = repoToken
inputs['allow-repeats'] = 'false'
const commentId = 123 const commentId = 123
@ -242,9 +338,7 @@ describe('add-pr-comment action', () => {
it('overrides the default message with a success message on success', async () => { it('overrides the default message with a success message on success', async () => {
inputs.message = simpleMessage inputs.message = simpleMessage
inputs['message-path'] = undefined
inputs['repo-token'] = repoToken
inputs['allow-repeats'] = 'false'
inputs['message-success'] = '666' inputs['message-success'] = '666'
inputs.status = 'success' inputs.status = 'success'
@ -265,9 +359,7 @@ describe('add-pr-comment action', () => {
it('overrides the default message with a failure message on failure', async () => { it('overrides the default message with a failure message on failure', async () => {
inputs.message = simpleMessage inputs.message = simpleMessage
inputs['message-path'] = undefined
inputs['repo-token'] = repoToken
inputs['allow-repeats'] = 'false'
inputs['message-failure'] = '666' inputs['message-failure'] = '666'
inputs.status = 'failure' inputs.status = 'failure'
@ -288,9 +380,7 @@ describe('add-pr-comment action', () => {
it('overrides the default message with a cancelled message on cancelled', async () => { it('overrides the default message with a cancelled message on cancelled', async () => {
inputs.message = simpleMessage inputs.message = simpleMessage
inputs['message-path'] = undefined
inputs['repo-token'] = repoToken
inputs['allow-repeats'] = 'false'
inputs['message-cancelled'] = '666' inputs['message-cancelled'] = '666'
inputs.status = 'cancelled' inputs.status = 'cancelled'
@ -311,9 +401,7 @@ describe('add-pr-comment action', () => {
it('overrides the default message with a skipped message on skipped', async () => { it('overrides the default message with a skipped message on skipped', async () => {
inputs.message = simpleMessage inputs.message = simpleMessage
inputs['message-path'] = undefined
inputs['repo-token'] = repoToken
inputs['allow-repeats'] = 'false'
inputs['message-skipped'] = '666' inputs['message-skipped'] = '666'
inputs.status = 'skipped' inputs.status = 'skipped'
@ -331,4 +419,212 @@ describe('add-pr-comment action', () => {
await run() await run()
expect(messagePayload?.body).toContain('666') expect(messagePayload?.body).toContain('666')
}) })
it('wraps a message in a codeblock if preformatted is true', async () => {
inputs.message = undefined
inputs['preformatted'] = 'true'
inputs['message-path'] = messagePath1Fixture
await expect(run()).resolves.not.toThrow()
expect(
`<!-- add-pr-comment:add-pr-comment -->\n\n\`\`\`\n${messagePath1FixturePayload}\n\`\`\``,
).toEqual(messagePayload?.body)
expect(core.setOutput).toHaveBeenCalledWith('comment-created', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', postIssueCommentsResponse.id)
})
})
describe('find and replace', () => {
it('can find and replace text in an existing comment', async () => {
inputs['find'] = 'world'
inputs['replace'] = 'mars'
const commentId = 123
const replyBody = [
{
id: commentId,
body: `<!-- add-pr-comment:${inputs['message-id']} -->\n\n${simpleMessage}`,
},
]
getIssueCommentsResponse = replyBody
postIssueCommentsResponse = {
id: commentId,
}
await run()
expect(`<!-- add-pr-comment:add-pr-comment -->\n\nhello mars`).toEqual(messagePayload?.body)
expect(core.setOutput).toHaveBeenCalledWith('comment-updated', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', commentId)
})
it('can multiple find and replace text in an existing comment', async () => {
inputs['find'] = 'hello\nworld'
inputs['replace'] = 'goodbye\nmars'
const body = `<!-- add-pr-comment:${inputs['message-id']} -->\n\nhello\nworld`
const commentId = 123
const replyBody = [
{
id: commentId,
body,
},
]
getIssueCommentsResponse = replyBody
postIssueCommentsResponse = {
id: commentId,
}
await run()
expect(`<!-- add-pr-comment:add-pr-comment -->\n\ngoodbye\nmars`).toEqual(messagePayload?.body)
expect(core.setOutput).toHaveBeenCalledWith('comment-updated', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', commentId)
})
it('can multiple find and replace text using a message', async () => {
inputs['find'] = 'hello\nworld'
inputs['message'] = 'mars'
const body = `<!-- add-pr-comment:${inputs['message-id']} -->\n\nhello\nworld`
const commentId = 123
const replyBody = [
{
id: commentId,
body,
},
]
getIssueCommentsResponse = replyBody
postIssueCommentsResponse = {
id: commentId,
}
await run()
expect(`<!-- add-pr-comment:add-pr-comment -->\n\nmars\nmars`).toEqual(messagePayload?.body)
expect(core.setOutput).toHaveBeenCalledWith('comment-updated', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', commentId)
})
it('can multiple find and replace a single pattern with a multiline replacement', async () => {
inputs['find'] = 'hello'
inputs['message'] = 'h\ne\nl\nl\no'
const body = `<!-- add-pr-comment:${inputs['message-id']} -->\n\nhello\nworld`
const commentId = 123
const replyBody = [
{
id: commentId,
body,
},
]
getIssueCommentsResponse = replyBody
postIssueCommentsResponse = {
id: commentId,
}
await run()
expect(`<!-- add-pr-comment:add-pr-comment -->\n\nh\ne\nl\nl\no\nworld`).toEqual(
messagePayload?.body,
)
expect(core.setOutput).toHaveBeenCalledWith('comment-updated', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', commentId)
})
it('can multiple find and replace text using a message-path', async () => {
inputs['find'] = '<< FILE_CONTENTS >>'
inputs['message-path'] = messagePath1Fixture
const body = `<!-- add-pr-comment:${inputs['message-id']} -->\n\nhello\n<< FILE_CONTENTS >>\nworld`
const commentId = 123
const replyBody = [
{
id: commentId,
body,
},
]
getIssueCommentsResponse = replyBody
postIssueCommentsResponse = {
id: commentId,
}
await run()
expect(
`<!-- add-pr-comment:add-pr-comment -->\n\nhello\n${messagePath1FixturePayload}\nworld`,
).toEqual(messagePayload?.body)
expect(core.setOutput).toHaveBeenCalledWith('comment-updated', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', commentId)
})
it('can find and replace patterns and use alternative modifiers', async () => {
inputs['find'] = '(o|l)/g'
inputs['replace'] = 'YY'
const body = `<!-- add-pr-comment:${inputs['message-id']} -->\n\nHELLO\nworld`
const commentId = 123
const replyBody = [
{
id: commentId,
body,
},
]
getIssueCommentsResponse = replyBody
postIssueCommentsResponse = {
id: commentId,
}
await run()
expect(`<!-- add-pr-comment:add-pr-comment -->\n\nHELLO\nwYYrYYd`).toEqual(messagePayload?.body)
expect(core.setOutput).toHaveBeenCalledWith('comment-updated', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', commentId)
})
it('can check some boxes with find and replace', async () => {
inputs['find'] = '\n\\[ \\]'
inputs['replace'] = '[X]'
const body = `<!-- add-pr-comment:${inputs['message-id']} -->\n\n[ ] Hello\n[ ] World`
const commentId = 123
const replyBody = [
{
id: commentId,
body,
},
]
getIssueCommentsResponse = replyBody
postIssueCommentsResponse = {
id: commentId,
}
await run()
expect(`<!-- add-pr-comment:add-pr-comment -->\n\n[X] Hello\n[X] World`).toEqual(
messagePayload?.body,
)
expect(core.setOutput).toHaveBeenCalledWith('comment-updated', 'true')
expect(core.setOutput).toHaveBeenCalledWith('comment-id', commentId)
})
}) })

View file

@ -0,0 +1,7 @@
## [Preview link](https://antares-blog-staging-pr-${{ github.event.number }}.azurewebsites.net)
- Your changes have been deployed to the preview site. The preview site will update as you add more commits to this branch.
- The preview site shows any future-dated articles. Don't worry, if you are publishing a future-dated article, it will not show on the production site until the file's specified date.
- The preview link is shareable, but will be deleted when the pull request is merged or closed.
> *This is an automated message.*

View file

@ -5,31 +5,39 @@ inputs:
description: "The message to print." description: "The message to print."
required: false required: false
message-path: message-path:
description: "A path to a file to print as a message instead of a string." description: "A path or list of paths to a file to print as a message instead of a string."
required: false required: false
message-id: message-id:
description: "An optional id to use for this message." description: "An optional id to use for this message."
default: "add-pr-comment" default: "add-pr-comment"
required: false required: true
refresh-message-position: refresh-message-position:
description: "If a message with the same id, this option allow to refresh the position of the message to be the last one posted." description: "If a message with the same id, this option allow to refresh the position of the message to be the last one posted."
default: "false" default: "false"
required: false required: true
repo-owner:
description: "The repo owner."
default: "${{ github.repository_owner }}"
required: true
repo-name:
description: "The repo name."
default: "${{ github.event.repository.name }}"
required: true
repo-token: repo-token:
description: "A GitHub token for API access. Defaults to {{ github.token }}." description: "A GitHub token for API access. Defaults to {{ github.token }}."
default: "${{ github.token }}" default: "${{ github.token }}"
required: false required: true
allow-repeats: allow-repeats:
description: "Allow messages to be repeated." description: "Allow messages to be repeated."
default: "false" default: "false"
required: false required: true
proxy-url: proxy-url:
description: "Proxy URL for comment creation" description: "Proxy URL for comment creation"
required: false required: false
status: status:
description: "A job status for status headers. Defaults to {{ job.status }}." description: "A job status for status headers. Defaults to {{ job.status }}."
default: "${{ job.status }}" default: "${{ job.status }}"
required: false required: true
message-success: message-success:
description: "Override the message when a run is successful." description: "Override the message when a run is successful."
required: false required: false
@ -45,6 +53,16 @@ inputs:
issue: issue:
description: "Override the message when a run is cancelled." description: "Override the message when a run is cancelled."
required: false required: false
update-only:
description: "Only update the comment if it already exists."
required: false
preformatted:
description: "Treat message text (from a file or input) as pre-formatted and place it in a codeblock."
required: false
find:
description: "A regular expression to find for replacement. Multiple lines become individual regular expressions."
replace:
description: "A replacement to use, overrides the message. Multple lines can replace same-indexed find patterns."
outputs: outputs:
comment-created: comment-created:
description: "Whether a comment was created." description: "Whether a comment was created."
@ -56,5 +74,5 @@ branding:
icon: message-circle icon: message-circle
color: purple color: purple
runs: runs:
using: "node16" using: "node20"
main: "dist/index.js" main: "dist/index.js"

2826
dist/index.js vendored

File diff suppressed because it is too large Load diff

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.createComment = exports.deleteComment = exports.updateComment = exports.getExistingCommentId = void 0; exports.createComment = exports.deleteComment = exports.updateComment = exports.getExistingComment = void 0;
async function getExistingCommentId(octokit, owner, repo, issueNumber, messageId) { async function getExistingComment(octokit, owner, repo, issueNumber, messageId) {
const parameters = { const parameters = {
owner, owner,
repo, repo,
@ -18,9 +18,13 @@ async function getExistingCommentId(octokit, owner, repo, issueNumber, messageId
break; break;
} }
} }
return found === null || found === void 0 ? void 0 : found.id; if (found) {
const { id, body } = found;
return { id, body };
} }
exports.getExistingCommentId = getExistingCommentId; return;
}
exports.getExistingComment = getExistingComment;
async function updateComment(octokit, owner, repo, existingCommentId, body) { async function updateComment(octokit, owner, repo, existingCommentId, body) {
const updatedComment = await octokit.rest.issues.updateComment({ const updatedComment = await octokit.rest.issues.updateComment({
comment_id: existingCommentId, comment_id: existingCommentId,

View file

@ -22,74 +22,58 @@ var __importStar = (this && this.__importStar) || function (mod) {
__setModuleDefault(result, mod); __setModuleDefault(result, mod);
return result; return result;
}; };
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.getInputs = void 0; exports.getInputs = void 0;
const core = __importStar(require("@actions/core")); const core = __importStar(require("@actions/core"));
const github = __importStar(require("@actions/github")); const github = __importStar(require("@actions/github"));
const promises_1 = __importDefault(require("node:fs/promises"));
async function getInputs() { async function getInputs() {
var _a, _b, _c; var _a, _b;
const messageIdInput = core.getInput('message-id', { required: false }); const messageIdInput = core.getInput('message-id', { required: false });
const messageId = messageIdInput === '' ? 'add-pr-comment' : `add-pr-comment:${messageIdInput}`; const messageId = messageIdInput === '' ? 'add-pr-comment' : `add-pr-comment:${messageIdInput}`;
const messageInput = core.getInput('message', { required: false }); const messageInput = core.getInput('message', { required: false });
const messagePath = core.getInput('message-path', { required: false }); const messagePath = core.getInput('message-path', { required: false });
const messageFind = core.getMultilineInput('find', { required: false });
const messageReplace = core.getMultilineInput('replace', { required: false });
const repoOwner = core.getInput('repo-owner', { required: true });
const repoName = core.getInput('repo-name', { required: true });
const repoToken = core.getInput('repo-token', { required: true }); const repoToken = core.getInput('repo-token', { required: true });
const status = core.getInput('status', { required: true }); const status = core.getInput('status', { required: true });
const issue = core.getInput('issue', { required: false }); const issue = core.getInput('issue', { required: false });
const proxyUrl = core.getInput('proxy-url', { required: false }).replace(/\/$/, ''); const proxyUrl = core.getInput('proxy-url', { required: false }).replace(/\/$/, '');
const allowRepeats = core.getInput('allow-repeats', { required: true }) === 'true'; const allowRepeats = core.getInput('allow-repeats', { required: true }) === 'true';
const refreshMessagePosition = core.getInput('refresh-message-position', { required: false }) === 'true'; const refreshMessagePosition = core.getInput('refresh-message-position', { required: false }) === 'true';
const updateOnly = core.getInput('update-only', { required: false }) === 'true';
const preformatted = core.getInput('preformatted', { required: false }) === 'true';
if (messageInput && messagePath) { if (messageInput && messagePath) {
throw new Error('must specify only one, message or message-path'); throw new Error('must specify only one, message or message-path');
} }
let message;
if (messagePath) {
message = await promises_1.default.readFile(messagePath, { encoding: 'utf8' });
}
else {
message = messageInput;
}
const messageSuccess = core.getInput(`message-success`); const messageSuccess = core.getInput(`message-success`);
const messageFailure = core.getInput(`message-failure`); const messageFailure = core.getInput(`message-failure`);
const messageCancelled = core.getInput(`message-cancelled`); const messageCancelled = core.getInput(`message-cancelled`);
const messageSkipped = core.getInput(`message-skipped`); const messageSkipped = core.getInput(`message-skipped`);
if (status === 'success' && messageSuccess) {
message = messageSuccess;
}
if (status === 'failure' && messageFailure) {
message = messageFailure;
}
if (status === 'cancelled' && messageCancelled) {
message = messageCancelled;
}
if (status === 'skipped' && messageSkipped) {
message = messageSkipped;
}
if (!message) {
throw new Error('no message, check your message inputs');
}
const { payload } = github.context; const { payload } = github.context;
const repoFullName = (_a = payload.repository) === null || _a === void 0 ? void 0 : _a.full_name;
if (!repoFullName) {
throw new Error('unable to determine repository from request type');
}
const [owner, repo] = repoFullName.split('/');
return { return {
refreshMessagePosition,
allowRepeats, allowRepeats,
message, commitSha: github.context.sha,
issue: issue ? Number(issue) : (_a = payload.issue) === null || _a === void 0 ? void 0 : _a.number,
messageInput,
messageId: `<!-- ${messageId} -->`, messageId: `<!-- ${messageId} -->`,
messageSuccess,
messageFailure,
messageCancelled,
messageSkipped,
messagePath,
messageFind,
messageReplace,
preformatted,
proxyUrl, proxyUrl,
pullRequestNumber: (_b = payload.pull_request) === null || _b === void 0 ? void 0 : _b.number,
refreshMessagePosition,
repoToken, repoToken,
status, status,
issue: issue ? Number(issue) : (_b = payload.issue) === null || _b === void 0 ? void 0 : _b.number, owner: repoOwner || payload.repo.owner,
pullRequestNumber: (_c = payload.pull_request) === null || _c === void 0 ? void 0 : _c.number, repo: repoName || payload.repo.repo,
commitSha: github.context.sha, updateOnly: updateOnly,
owner,
repo,
}; };
} }
exports.getInputs = getInputs; exports.getInputs = getInputs;

53
lib/files.js Normal file
View file

@ -0,0 +1,53 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.findFiles = void 0;
const core = __importStar(require("@actions/core"));
const glob = __importStar(require("@actions/glob"));
const promises_1 = __importDefault(require("node:fs/promises"));
async function findFiles(searchPath) {
const searchResults = [];
const globber = await glob.create(searchPath, {
followSymbolicLinks: true,
implicitDescendants: true,
omitBrokenSymbolicLinks: true,
});
const rawSearchResults = await globber.glob();
for (const searchResult of rawSearchResults) {
const fileStats = await promises_1.default.stat(searchResult);
if (!fileStats.isDirectory()) {
core.debug(`File: ${searchResult} was found using the provided searchPath`);
searchResults.push(searchResult);
}
else {
core.debug(`Removing ${searchResult} from rawSearchResults because it is a directory`);
}
}
return searchResults;
}
exports.findFiles = findFiles;

View file

@ -28,11 +28,22 @@ const github = __importStar(require("@actions/github"));
const comments_1 = require("./comments"); const comments_1 = require("./comments");
const config_1 = require("./config"); const config_1 = require("./config");
const issues_1 = require("./issues"); const issues_1 = require("./issues");
const message_1 = require("./message");
const proxy_1 = require("./proxy"); const proxy_1 = require("./proxy");
const run = async () => { const run = async () => {
try { try {
const { allowRepeats, message, messageId, refreshMessagePosition, repoToken, proxyUrl, issue, pullRequestNumber, commitSha, repo, owner, } = await (0, config_1.getInputs)(); const { allowRepeats, messagePath, messageInput, messageId, refreshMessagePosition, repoToken, proxyUrl, issue, pullRequestNumber, commitSha, repo, owner, updateOnly, messageCancelled, messageFailure, messageSuccess, messageSkipped, preformatted, status, messageFind, messageReplace, } = await (0, config_1.getInputs)();
const octokit = github.getOctokit(repoToken); const octokit = github.getOctokit(repoToken);
let message = await (0, message_1.getMessage)({
messagePath,
messageInput,
messageSkipped,
messageCancelled,
messageSuccess,
messageFailure,
preformatted,
status,
});
let issueNumber; let issueNumber;
if (issue) { if (issue) {
issueNumber = issue; issueNumber = issue;
@ -49,19 +60,31 @@ const run = async () => {
core.setOutput('comment-created', 'false'); core.setOutput('comment-created', 'false');
return; return;
} }
let existingCommentId; let existingComment;
if (!allowRepeats) { if (!allowRepeats) {
core.debug('repeat comments are disallowed, checking for existing'); core.debug('repeat comments are disallowed, checking for existing');
existingCommentId = await (0, comments_1.getExistingCommentId)(octokit, owner, repo, issueNumber, messageId); existingComment = await (0, comments_1.getExistingComment)(octokit, owner, repo, issueNumber, messageId);
if (existingCommentId) { if (existingComment) {
core.debug(`existing comment found with id: ${existingCommentId}`); core.debug(`existing comment found with id: ${existingComment.id}`);
} }
} }
// if no existing comment and updateOnly is true, exit
if (!existingComment && updateOnly) {
core.info('no existing comment found and update-only is true, exiting');
core.setOutput('comment-created', 'false');
return;
}
let comment; let comment;
const body = `${messageId}\n\n${message}`; if ((messageFind === null || messageFind === void 0 ? void 0 : messageFind.length) && ((messageReplace === null || messageReplace === void 0 ? void 0 : messageReplace.length) || message) && (existingComment === null || existingComment === void 0 ? void 0 : existingComment.body)) {
message = (0, message_1.findAndReplaceInMessage)(messageFind, (messageReplace === null || messageReplace === void 0 ? void 0 : messageReplace.length) ? messageReplace : [message], (0, message_1.removeMessageHeader)(existingComment.body));
}
if (!message) {
throw new Error('no message, check your message inputs');
}
const body = (0, message_1.addMessageHeader)(messageId, message);
if (proxyUrl) { if (proxyUrl) {
comment = await (0, proxy_1.createCommentProxy)({ comment = await (0, proxy_1.createCommentProxy)({
commentId: existingCommentId, commentId: existingComment === null || existingComment === void 0 ? void 0 : existingComment.id,
owner, owner,
repo, repo,
issueNumber, issueNumber,
@ -69,15 +92,15 @@ const run = async () => {
repoToken, repoToken,
proxyUrl, proxyUrl,
}); });
core.setOutput(existingCommentId ? 'comment-updated' : 'comment-created', 'true'); core.setOutput((existingComment === null || existingComment === void 0 ? void 0 : existingComment.id) ? 'comment-updated' : 'comment-created', 'true');
} }
else if (existingCommentId) { else if (existingComment === null || existingComment === void 0 ? void 0 : existingComment.id) {
if (refreshMessagePosition) { if (refreshMessagePosition) {
await (0, comments_1.deleteComment)(octokit, owner, repo, existingCommentId, body); await (0, comments_1.deleteComment)(octokit, owner, repo, existingComment.id, body);
comment = await (0, comments_1.createComment)(octokit, owner, repo, issueNumber, body); comment = await (0, comments_1.createComment)(octokit, owner, repo, issueNumber, body);
} }
else { else {
comment = await (0, comments_1.updateComment)(octokit, owner, repo, existingCommentId, body); comment = await (0, comments_1.updateComment)(octokit, owner, repo, existingComment.id, body);
} }
core.setOutput('comment-updated', 'true'); core.setOutput('comment-updated', 'true');
} }
@ -94,6 +117,10 @@ const run = async () => {
} }
} }
catch (err) { catch (err) {
if (process.env.NODE_ENV === 'test') {
// eslint-disable-next-line no-console
console.log(err);
}
if (err instanceof Error) { if (err instanceof Error) {
core.setFailed(err.message); core.setFailed(err.message);
} }

81
lib/message.js Normal file
View file

@ -0,0 +1,81 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.findAndReplaceInMessage = exports.removeMessageHeader = exports.addMessageHeader = exports.getMessageFromPath = exports.getMessage = void 0;
const promises_1 = __importDefault(require("node:fs/promises"));
const files_1 = require("./files");
async function getMessage({ messageInput, messagePath, messageCancelled, messageSkipped, messageFailure, messageSuccess, preformatted, status, }) {
let message;
if (status === 'success' && messageSuccess) {
message = messageSuccess;
}
if (status === 'failure' && messageFailure) {
message = messageFailure;
}
if (status === 'cancelled' && messageCancelled) {
message = messageCancelled;
}
if (status === 'skipped' && messageSkipped) {
message = messageSkipped;
}
if (!message) {
if (messagePath) {
message = await getMessageFromPath(messagePath);
}
else {
message = messageInput;
}
}
if (preformatted) {
message = `\`\`\`\n${message}\n\`\`\``;
}
return message !== null && message !== void 0 ? message : '';
}
exports.getMessage = getMessage;
async function getMessageFromPath(searchPath) {
let message = '';
const files = await (0, files_1.findFiles)(searchPath);
for (const [index, path] of files.entries()) {
if (index > 0) {
message += '\n';
}
message += await promises_1.default.readFile(path, { encoding: 'utf8' });
}
return message;
}
exports.getMessageFromPath = getMessageFromPath;
function addMessageHeader(messageId, message) {
return `${messageId}\n\n${message}`;
}
exports.addMessageHeader = addMessageHeader;
function removeMessageHeader(message) {
return message.split('\n').slice(2).join('\n');
}
exports.removeMessageHeader = removeMessageHeader;
function splitFind(find) {
const matches = find.match(/\/((i|g|m|s|u|y){1,6})$/);
if (!matches) {
return {
regExp: find,
modifiers: 'gi',
};
}
const [, modifiers] = matches;
const regExp = find.replace(modifiers, '').slice(0, -1);
return {
regExp,
modifiers,
};
}
function findAndReplaceInMessage(find, replace, original) {
var _a;
let message = original;
for (const [i, f] of find.entries()) {
const { regExp, modifiers } = splitFind(f);
message = message.replace(new RegExp(regExp, modifiers), (_a = replace[i]) !== null && _a !== void 0 ? _a : replace.join('\n'));
}
return message;
}
exports.findAndReplaceInMessage = findAndReplaceInMessage;

2
lib/types.js Normal file
View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

68
lib/util.js Normal file
View file

@ -0,0 +1,68 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.findFiles = exports.getMessageFromPaths = void 0;
const core = __importStar(require("@actions/core"));
const glob = __importStar(require("@actions/glob"));
const promises_1 = __importDefault(require("node:fs/promises"));
async function getMessageFromPaths(searchPath) {
let message = '';
const files = await findFiles(searchPath);
for (const [index, path] of files.entries()) {
if (index > 0) {
message += '\n';
}
message += await promises_1.default.readFile(path, { encoding: 'utf8' });
}
return message;
}
exports.getMessageFromPaths = getMessageFromPaths;
function getDefaultGlobOptions() {
return {
followSymbolicLinks: true,
implicitDescendants: true,
omitBrokenSymbolicLinks: true,
};
}
async function findFiles(searchPath, globOptions) {
const searchResults = [];
const globber = await glob.create(searchPath, globOptions || getDefaultGlobOptions());
const rawSearchResults = await globber.glob();
for (const searchResult of rawSearchResults) {
const fileStats = await promises_1.default.stat(searchResult);
if (!fileStats.isDirectory()) {
core.debug(`File: ${searchResult} was found using the provided searchPath`);
searchResults.push(searchResult);
}
else {
core.debug(`Removing ${searchResult} from rawSearchResults because it is a directory`);
}
}
return searchResults;
}
exports.findFiles = findFiles;

11877
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "@mshick/add-pr-comment", "name": "@mshick/add-pr-comment",
"version": "2.2.0", "version": "2.8.2",
"description": "A GitHub Action which adds a comment to a Pull Request Issue.", "description": "A GitHub Action which adds a comment to a Pull Request Issue.",
"keywords": [ "keywords": [
"GitHub", "GitHub",
@ -25,7 +25,8 @@
"build": "del-cli dist && tsc && ncc build --source-map", "build": "del-cli dist && tsc && ncc build --source-map",
"clean": "rm -rf node_modules dist package-lock.json __tests__/runner/**/*", "clean": "rm -rf node_modules dist package-lock.json __tests__/runner/**/*",
"lint": "eslint src/**/*.ts", "lint": "eslint src/**/*.ts",
"release": "np --no-publish", "prepare": "npm run build && git add lib dist",
"release": "npm run build && np --no-publish",
"test": "vitest run", "test": "vitest run",
"watch": "vitest" "watch": "vitest"
}, },
@ -73,6 +74,23 @@
"@typescript-eslint/no-explicit-any": "off" "@typescript-eslint/no-explicit-any": "off"
} }
}, },
{
"files": [
"**/*.test.ts"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"extends": [
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off"
}
},
{ {
"files": [ "files": [
"*.json" "*.json"
@ -102,32 +120,34 @@
"dist" "dist"
], ],
"dependencies": { "dependencies": {
"@actions/artifact": "^1.1.1",
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
"@actions/github": "^5.1.1", "@actions/github": "^5.1.1",
"@actions/http-client": "^2.0.1" "@actions/glob": "^0.4.0",
"@actions/http-client": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"@octokit/types": "^8.0.0", "@octokit/types": "^9.1.2",
"@types/node": "^18.11.9", "@types/node": "^18.15.13",
"@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.42.0", "@typescript-eslint/parser": "^5.59.0",
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.36.1",
"del-cli": "^5.0.0", "del-cli": "^5.0.0",
"eslint": "^8.26.0", "eslint": "^8.39.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-typescript": "^3.5.2", "eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-json-format": "^2.0.1", "eslint-plugin-json-format": "^2.0.1",
"eslint-plugin-mdx": "^2.0.5", "eslint-plugin-mdx": "^2.0.5",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"msw": "^0.47.4", "msw": "^1.2.1",
"nock": "^13.2.9", "nock": "^13.3.0",
"np": "^7.7.0", "np": "^7.7.0",
"prettier": "^2.7.1", "prettier": "^2.8.7",
"typescript": "^4.8.4", "typescript": "^5.0.4",
"vitest": "^0.24.5" "vitest": "^0.30.1"
}, },
"engines": { "engines": {
"node": "^14.15.0 || ^16.13.0 || ^18.0.0" "node": "^16.13.0 || ^18.0.0 || ^20.0.0"
} }
} }

View file

@ -1,16 +1,17 @@
import { GitHub } from '@actions/github/lib/utils' import { GitHub } from '@actions/github/lib/utils'
import { Endpoints } from '@octokit/types' import {
CreateIssueCommentResponseData,
ExistingIssueComment,
ExistingIssueCommentResponseData,
} from './types'
export type CreateIssueCommentResponseData = export async function getExistingComment(
Endpoints['POST /repos/{owner}/{repo}/issues/{issue_number}/comments']['response']['data']
export async function getExistingCommentId(
octokit: InstanceType<typeof GitHub>, octokit: InstanceType<typeof GitHub>,
owner: string, owner: string,
repo: string, repo: string,
issueNumber: number, issueNumber: number,
messageId: string, messageId: string,
): Promise<number | undefined> { ): Promise<ExistingIssueComment | undefined> {
const parameters = { const parameters = {
owner, owner,
repo, repo,
@ -18,7 +19,7 @@ export async function getExistingCommentId(
per_page: 100, per_page: 100,
} }
let found let found: ExistingIssueCommentResponseData | undefined
for await (const comments of octokit.paginate.iterator( for await (const comments of octokit.paginate.iterator(
octokit.rest.issues.listComments, octokit.rest.issues.listComments,
@ -33,7 +34,12 @@ export async function getExistingCommentId(
} }
} }
return found?.id if (found) {
const { id, body } = found
return { id, body }
}
return
} }
export async function updateComment( export async function updateComment(

View file

@ -1,31 +1,16 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import fs from 'node:fs/promises' import { Inputs } from './types'
interface Inputs {
refreshMessagePosition: boolean
allowRepeats: boolean
message?: string
messageId: string
messagePath?: string
messageSuccess?: string
messageFailure?: string
messageCancelled?: string
proxyUrl?: string
repoToken: string
status?: string
issue?: number
commitSha: string
pullRequestNumber?: number
repo: string
owner: string
}
export async function getInputs(): Promise<Inputs> { export async function getInputs(): Promise<Inputs> {
const messageIdInput = core.getInput('message-id', { required: false }) const messageIdInput = core.getInput('message-id', { required: false })
const messageId = messageIdInput === '' ? 'add-pr-comment' : `add-pr-comment:${messageIdInput}` const messageId = messageIdInput === '' ? 'add-pr-comment' : `add-pr-comment:${messageIdInput}`
const messageInput = core.getInput('message', { required: false }) const messageInput = core.getInput('message', { required: false })
const messagePath = core.getInput('message-path', { required: false }) const messagePath = core.getInput('message-path', { required: false })
const messageFind = core.getMultilineInput('find', { required: false })
const messageReplace = core.getMultilineInput('replace', { required: false })
const repoOwner = core.getInput('repo-owner', { required: true })
const repoName = core.getInput('repo-name', { required: true })
const repoToken = core.getInput('repo-token', { required: true }) const repoToken = core.getInput('repo-token', { required: true })
const status = core.getInput('status', { required: true }) const status = core.getInput('status', { required: true })
const issue = core.getInput('issue', { required: false }) const issue = core.getInput('issue', { required: false })
@ -33,66 +18,41 @@ export async function getInputs(): Promise<Inputs> {
const allowRepeats = core.getInput('allow-repeats', { required: true }) === 'true' const allowRepeats = core.getInput('allow-repeats', { required: true }) === 'true'
const refreshMessagePosition = const refreshMessagePosition =
core.getInput('refresh-message-position', { required: false }) === 'true' core.getInput('refresh-message-position', { required: false }) === 'true'
const updateOnly = core.getInput('update-only', { required: false }) === 'true'
const preformatted = core.getInput('preformatted', { required: false }) === 'true'
if (messageInput && messagePath) { if (messageInput && messagePath) {
throw new Error('must specify only one, message or message-path') throw new Error('must specify only one, message or message-path')
} }
let message
if (messagePath) {
message = await fs.readFile(messagePath, { encoding: 'utf8' })
} else {
message = messageInput
}
const messageSuccess = core.getInput(`message-success`) const messageSuccess = core.getInput(`message-success`)
const messageFailure = core.getInput(`message-failure`) const messageFailure = core.getInput(`message-failure`)
const messageCancelled = core.getInput(`message-cancelled`) const messageCancelled = core.getInput(`message-cancelled`)
const messageSkipped = core.getInput(`message-skipped`) const messageSkipped = core.getInput(`message-skipped`)
if (status === 'success' && messageSuccess) {
message = messageSuccess
}
if (status === 'failure' && messageFailure) {
message = messageFailure
}
if (status === 'cancelled' && messageCancelled) {
message = messageCancelled
}
if (status === 'skipped' && messageSkipped) {
message = messageSkipped
}
if (!message) {
throw new Error('no message, check your message inputs')
}
const { payload } = github.context const { payload } = github.context
const repoFullName = payload.repository?.full_name
if (!repoFullName) {
throw new Error('unable to determine repository from request type')
}
const [owner, repo] = repoFullName.split('/')
return { return {
refreshMessagePosition,
allowRepeats, allowRepeats,
message, commitSha: github.context.sha,
issue: issue ? Number(issue) : payload.issue?.number,
messageInput,
messageId: `<!-- ${messageId} -->`, messageId: `<!-- ${messageId} -->`,
messageSuccess,
messageFailure,
messageCancelled,
messageSkipped,
messagePath,
messageFind,
messageReplace,
preformatted,
proxyUrl, proxyUrl,
pullRequestNumber: payload.pull_request?.number,
refreshMessagePosition,
repoToken, repoToken,
status, status,
issue: issue ? Number(issue) : payload.issue?.number, owner: repoOwner || payload.repo.owner,
pullRequestNumber: payload.pull_request?.number, repo: repoName || payload.repo.repo,
commitSha: github.context.sha, updateOnly: updateOnly,
owner,
repo,
} }
} }

25
src/files.ts Normal file
View file

@ -0,0 +1,25 @@
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import fs from 'node:fs/promises'
export async function findFiles(searchPath: string): Promise<string[]> {
const searchResults: string[] = []
const globber = await glob.create(searchPath, {
followSymbolicLinks: true,
implicitDescendants: true,
omitBrokenSymbolicLinks: true,
})
const rawSearchResults: string[] = await globber.glob()
for (const searchResult of rawSearchResults) {
const fileStats = await fs.stat(searchResult)
if (!fileStats.isDirectory()) {
core.debug(`File: ${searchResult} was found using the provided searchPath`)
searchResults.push(searchResult)
} else {
core.debug(`Removing ${searchResult} from rawSearchResults because it is a directory`)
}
}
return searchResults
}

View file

@ -1,21 +1,23 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import { import { createComment, deleteComment, getExistingComment, updateComment } from './comments'
createComment,
CreateIssueCommentResponseData,
getExistingCommentId,
updateComment,
deleteComment,
} from './comments'
import { getInputs } from './config' import { getInputs } from './config'
import { getIssueNumberFromCommitPullsList } from './issues' import { getIssueNumberFromCommitPullsList } from './issues'
import {
addMessageHeader,
findAndReplaceInMessage,
getMessage,
removeMessageHeader,
} from './message'
import { createCommentProxy } from './proxy' import { createCommentProxy } from './proxy'
import { CreateIssueCommentResponseData, ExistingIssueComment } from './types'
const run = async (): Promise<void> => { const run = async (): Promise<void> => {
try { try {
const { const {
allowRepeats, allowRepeats,
message, messagePath,
messageInput,
messageId, messageId,
refreshMessagePosition, refreshMessagePosition,
repoToken, repoToken,
@ -25,10 +27,30 @@ const run = async (): Promise<void> => {
commitSha, commitSha,
repo, repo,
owner, owner,
updateOnly,
messageCancelled,
messageFailure,
messageSuccess,
messageSkipped,
preformatted,
status,
messageFind,
messageReplace,
} = await getInputs() } = await getInputs()
const octokit = github.getOctokit(repoToken) const octokit = github.getOctokit(repoToken)
let message = await getMessage({
messagePath,
messageInput,
messageSkipped,
messageCancelled,
messageSuccess,
messageFailure,
preformatted,
status,
})
let issueNumber let issueNumber
if (issue) { if (issue) {
@ -48,25 +70,44 @@ const run = async (): Promise<void> => {
return return
} }
let existingCommentId let existingComment: ExistingIssueComment | undefined
if (!allowRepeats) { if (!allowRepeats) {
core.debug('repeat comments are disallowed, checking for existing') core.debug('repeat comments are disallowed, checking for existing')
existingCommentId = await getExistingCommentId(octokit, owner, repo, issueNumber, messageId) existingComment = await getExistingComment(octokit, owner, repo, issueNumber, messageId)
if (existingCommentId) { if (existingComment) {
core.debug(`existing comment found with id: ${existingCommentId}`) core.debug(`existing comment found with id: ${existingComment.id}`)
} }
} }
// if no existing comment and updateOnly is true, exit
if (!existingComment && updateOnly) {
core.info('no existing comment found and update-only is true, exiting')
core.setOutput('comment-created', 'false')
return
}
let comment: CreateIssueCommentResponseData | null | undefined let comment: CreateIssueCommentResponseData | null | undefined
const body = `${messageId}\n\n${message}` if (messageFind?.length && (messageReplace?.length || message) && existingComment?.body) {
message = findAndReplaceInMessage(
messageFind,
messageReplace?.length ? messageReplace : [message],
removeMessageHeader(existingComment.body),
)
}
if (!message) {
throw new Error('no message, check your message inputs')
}
const body = addMessageHeader(messageId, message)
if (proxyUrl) { if (proxyUrl) {
comment = await createCommentProxy({ comment = await createCommentProxy({
commentId: existingCommentId, commentId: existingComment?.id,
owner, owner,
repo, repo,
issueNumber, issueNumber,
@ -74,13 +115,13 @@ const run = async (): Promise<void> => {
repoToken, repoToken,
proxyUrl, proxyUrl,
}) })
core.setOutput(existingCommentId ? 'comment-updated' : 'comment-created', 'true') core.setOutput(existingComment?.id ? 'comment-updated' : 'comment-created', 'true')
} else if (existingCommentId) { } else if (existingComment?.id) {
if (refreshMessagePosition) { if (refreshMessagePosition) {
await deleteComment(octokit, owner, repo, existingCommentId, body) await deleteComment(octokit, owner, repo, existingComment.id, body)
comment = await createComment(octokit, owner, repo, issueNumber, body) comment = await createComment(octokit, owner, repo, issueNumber, body)
} else { } else {
comment = await updateComment(octokit, owner, repo, existingCommentId, body) comment = await updateComment(octokit, owner, repo, existingComment.id, body)
} }
core.setOutput('comment-updated', 'true') core.setOutput('comment-updated', 'true')
} else { } else {
@ -95,6 +136,11 @@ const run = async (): Promise<void> => {
core.setOutput('comment-updated', 'false') core.setOutput('comment-updated', 'false')
} }
} catch (err) { } catch (err) {
if (process.env.NODE_ENV === 'test') {
// eslint-disable-next-line no-console
console.log(err)
}
if (err instanceof Error) { if (err instanceof Error) {
core.setFailed(err.message) core.setFailed(err.message)
} }

114
src/message.ts Normal file
View file

@ -0,0 +1,114 @@
import fs from 'node:fs/promises'
import { findFiles } from './files'
import { Inputs } from './types'
export async function getMessage({
messageInput,
messagePath,
messageCancelled,
messageSkipped,
messageFailure,
messageSuccess,
preformatted,
status,
}: Pick<
Inputs,
| 'messageInput'
| 'messageCancelled'
| 'messageSuccess'
| 'messageFailure'
| 'messageSkipped'
| 'messagePath'
| 'preformatted'
| 'status'
>): Promise<string> {
let message
if (status === 'success' && messageSuccess) {
message = messageSuccess
}
if (status === 'failure' && messageFailure) {
message = messageFailure
}
if (status === 'cancelled' && messageCancelled) {
message = messageCancelled
}
if (status === 'skipped' && messageSkipped) {
message = messageSkipped
}
if (!message) {
if (messagePath) {
message = await getMessageFromPath(messagePath)
} else {
message = messageInput
}
}
if (preformatted) {
message = `\`\`\`\n${message}\n\`\`\``
}
return message ?? ''
}
export async function getMessageFromPath(searchPath: string) {
let message = ''
const files = await findFiles(searchPath)
for (const [index, path] of files.entries()) {
if (index > 0) {
message += '\n'
}
message += await fs.readFile(path, { encoding: 'utf8' })
}
return message
}
export function addMessageHeader(messageId: string, message: string) {
return `${messageId}\n\n${message}`
}
export function removeMessageHeader(message: string) {
return message.split('\n').slice(2).join('\n')
}
function splitFind(find: string) {
const matches = find.match(/\/((i|g|m|s|u|y){1,6})$/)
if (!matches) {
return {
regExp: find,
modifiers: 'gi',
}
}
const [, modifiers] = matches
const regExp = find.replace(modifiers, '').slice(0, -1)
return {
regExp,
modifiers,
}
}
export function findAndReplaceInMessage(
find: string[],
replace: string[],
original: string,
): string {
let message = original
for (const [i, f] of find.entries()) {
const { regExp, modifiers } = splitFind(f)
message = message.replace(new RegExp(regExp, modifiers), replace[i] ?? replace.join('\n'))
}
return message
}

34
src/types.ts Normal file
View file

@ -0,0 +1,34 @@
import { Endpoints } from '@octokit/types'
export interface Inputs {
allowRepeats: boolean
attachPath?: string[]
commitSha: string
issue?: number
messageInput?: string
messageId: string
messagePath?: string
messageFind?: string[]
messageReplace?: string[]
messageSuccess?: string
messageFailure?: string
messageCancelled?: string
messageSkipped?: string
preformatted: boolean
proxyUrl?: string
pullRequestNumber?: number
refreshMessagePosition: boolean
repo: string
repoToken: string
status: string
owner: string
updateOnly: boolean
}
export type CreateIssueCommentResponseData =
Endpoints['POST /repos/{owner}/{repo}/issues/{issue_number}/comments']['response']['data']
export type ExistingIssueCommentResponseData =
Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}/comments']['response']['data'][0]
export type ExistingIssueComment = Pick<ExistingIssueCommentResponseData, 'id' | 'body'>

View file

@ -12,7 +12,7 @@
"removeComments": false, "removeComments": false,
"preserveConstEnums": true, "preserveConstEnums": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"rootDir": "./src", "rootDir": "src",
"outDir": "./lib" "outDir": "./lib"
}, },
"exclude": ["node_modules", "**/*.test.ts"] "exclude": ["node_modules", "**/*.test.ts"]