Multiline message-path and concatenation (#88)

This commit is contained in:
Michael Shick 2023-05-04 17:25:26 -04:00 committed by GitHub
parent a0c6c0cbf4
commit 4a541a260f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 249 additions and 54 deletions

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

@ -62,7 +62,14 @@ jobs:
- uses: ./
with:
message-id: path
message-path: |
.github/test/file-*.txt
- uses: ./
with:
message-id: text
message: |
**Hello ${{ github.run_number }}**
**Hello**
🌏
!
!

View file

@ -1,6 +1,9 @@
# add-pr-comment
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
A GitHub Action which adds a comment to a pull request's issue.
@ -65,25 +68,25 @@ jobs:
## Configuration options
| Input | Location | Description | Required | Default |
|--------------------------|----------|------------------------------------------------------------------------------------------------------|----------|------------------------------------|
| 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-success | with | A message override, printed in case of success. | 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-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 }} |
| 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 }} |
| 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 |
| 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 | |
| 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 | |
| Input | Location | Description | Required | Default |
| ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------- |
| 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. 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-failure | with | A message override, printed in case of failure. | 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 | |
| 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 }} |
| 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 |
| 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 | |
| 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 | |
## Advanced Uses
@ -141,6 +144,31 @@ jobs:
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
```
### Bring your own issues
You can set an issue id explicitly. Helpful for cases where you want to post
@ -173,7 +201,7 @@ jobs:
issue: ${{ steps.pr.outputs.issue }}
message: |
**Howdie!**
```
````
## Contributors ✨

View file

@ -3,11 +3,16 @@ import * as github from '@actions/github'
import { WebhookPayload } from '@actions/github/lib/interfaces'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import run from '../src/main'
import apiResponse from './sample-pulls-api-response.json'
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 commitSha = 'abc123'
const simpleMessage = 'hello world'
@ -89,12 +94,19 @@ const handlers = [
const server = setupServer(...handlers)
describe('add-pr-comment action', () => {
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
beforeAll(() => {
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(core, 'debug').mockImplementation(() => {})
vi.spyOn(core, 'info').mockImplementation(() => {})
vi.spyOn(core, 'warning').mockImplementation(() => {})
server.listen({ onUnhandledRequest: 'error' })
})
afterAll(() => server.close())
beforeEach(() => {
inputs = { ...defaultInputs }
issueNumber = defaultIssueNumber
messagePayload = undefined
vi.resetModules()
@ -141,17 +153,46 @@ describe('add-pr-comment action', () => {
it('creates a comment with a message-path', async () => {
inputs.message = undefined
inputs['message-path'] = path.resolve(__dirname, './message.txt')
inputs['message-path'] = messagePath1Fixture
inputs['allow-repeats'] = 'true'
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-id', postIssueCommentsResponse.id)
})
it('fails when both message and message-path are defined', async () => {
inputs.message = 'foobar'
inputs['message-path'] = path.resolve(__dirname, './message.txt')
inputs['message-path'] = messagePath1Fixture
await expect(run()).resolves.not.toThrow()
expect(core.setFailed).toHaveBeenCalledWith('must specify only one, message or message-path')

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,7 +5,7 @@ inputs:
description: "The message to print."
required: false
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
message-id:
description: "An optional id to use for this message."

70
package-lock.json generated
View file

@ -9,8 +9,10 @@
"version": "2.4.0",
"license": "MIT",
"dependencies": {
"@actions/artifact": "^1.1.1",
"@actions/core": "^1.10.0",
"@actions/github": "^5.1.1",
"@actions/glob": "^0.4.0",
"@actions/http-client": "^2.1.0"
},
"devDependencies": {
@ -38,6 +40,28 @@
"node": "^14.15.0 || ^16.13.0 || ^18.0.0"
}
},
"node_modules/@actions/artifact": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/artifact/-/artifact-1.1.1.tgz",
"integrity": "sha512-Vv4y0EW0ptEkU+Pjs5RGS/0EryTvI6s79LjSV9Gg/h+O3H/ddpjhuX/Bi/HZE4pbNPyjGtQjbdFWphkZhmgabA==",
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/http-client": "^2.0.1",
"tmp": "^0.2.1",
"tmp-promise": "^3.0.2"
}
},
"node_modules/@actions/artifact/node_modules/tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"dependencies": {
"rimraf": "^3.0.0"
},
"engines": {
"node": ">=8.17.0"
}
},
"node_modules/@actions/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
@ -58,6 +82,15 @@
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
}
},
"node_modules/@actions/glob": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.4.0.tgz",
"integrity": "sha512-+eKIGFhsFa4EBwaf/GMyzCdWrXWymGXfFmZU3FHQvYS8mPcHtTtZONbkcqqUMzw9mJ/pImEBFET1JNifhqGsAQ==",
"dependencies": {
"@actions/core": "^1.9.1",
"minimatch": "^3.0.4"
}
},
"node_modules/@actions/http-client": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.0.tgz",
@ -1799,8 +1832,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-js": {
"version": "1.5.1",
@ -1879,7 +1911,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -2405,8 +2436,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/concordance": {
"version": "5.0.4",
@ -3753,8 +3783,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.2",
@ -3891,7 +3920,6 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -4427,7 +4455,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@ -4436,8 +4463,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "1.3.7",
@ -7483,7 +7509,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -8921,7 +8946,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -9618,7 +9642,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
@ -10312,6 +10335,25 @@
"node": ">=0.6.0"
}
},
"node_modules/tmp-promise": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
"dependencies": {
"tmp": "^0.2.0"
}
},
"node_modules/tmp-promise/node_modules/tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"dependencies": {
"rimraf": "^3.0.0"
},
"engines": {
"node": ">=8.17.0"
}
},
"node_modules/to-readable-stream": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-2.1.0.tgz",

View file

@ -74,6 +74,23 @@
"@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": [
"*.json"
@ -103,8 +120,10 @@
"dist"
],
"dependencies": {
"@actions/artifact": "^1.1.1",
"@actions/core": "^1.10.0",
"@actions/github": "^5.1.1",
"@actions/glob": "^0.4.0",
"@actions/http-client": "^2.1.0"
},
"devDependencies": {

View file

@ -1,10 +1,12 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import fs from 'node:fs/promises'
import { getMessageFromPaths } from './util'
interface Inputs {
refreshMessagePosition: boolean
allowRepeats: boolean
attachPath?: string[]
commitSha: string
issue?: number
message?: string
messageId: string
messagePath?: string
@ -12,12 +14,11 @@ interface Inputs {
messageFailure?: string
messageCancelled?: string
proxyUrl?: string
pullRequestNumber?: number
refreshMessagePosition: boolean
repo: string
repoToken: string
status?: string
issue?: number
commitSha: string
pullRequestNumber?: number
repo: string
owner: string
updateOnly: boolean
}
@ -38,14 +39,14 @@ export async function getInputs(): Promise<Inputs> {
core.getInput('refresh-message-position', { required: false }) === 'true'
const updateOnly = core.getInput('update-only', { required: false }) === 'true'
if (messageInput && messagePath) {
if (messageInput && messagePath.length) {
throw new Error('must specify only one, message or message-path')
}
let message
if (messagePath) {
message = await fs.readFile(messagePath, { encoding: 'utf8' })
if (messagePath.length) {
message = await getMessageFromPaths(messagePath)
} else {
message = messageInput
}
@ -78,16 +79,16 @@ export async function getInputs(): Promise<Inputs> {
const { payload } = github.context
return {
refreshMessagePosition,
allowRepeats,
commitSha: github.context.sha,
issue: issue ? Number(issue) : payload.issue?.number,
message,
messageId: `<!-- ${messageId} -->`,
proxyUrl,
pullRequestNumber: payload.pull_request?.number,
refreshMessagePosition,
repoToken,
status,
issue: issue ? Number(issue) : payload.issue?.number,
pullRequestNumber: payload.pull_request?.number,
commitSha: github.context.sha,
owner: repoOwner || payload.repo.owner,
repo: repoName || payload.repo.repo,
updateOnly: updateOnly,

48
src/util.ts Normal file
View file

@ -0,0 +1,48 @@
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import fs from 'node:fs/promises'
export async function getMessageFromPaths(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
}
function getDefaultGlobOptions(): glob.GlobOptions {
return {
followSymbolicLinks: true,
implicitDescendants: true,
omitBrokenSymbolicLinks: true,
}
}
export async function findFiles(
searchPath: string,
globOptions?: glob.GlobOptions,
): Promise<string[]> {
const searchResults: string[] = []
const globber = await glob.create(searchPath, globOptions || getDefaultGlobOptions())
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

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