├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── automerge-dependabot.yml │ ├── ci.yml │ ├── ciff-example-command.yml │ ├── slash-command-dispatch.yml │ └── update-major-version.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── action.yml ├── dist └── index.js ├── example-content └── output.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── main.ts └── utils.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "node": true, "jest": true }, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "ecmaVersion": 9, "sourceType": "module" }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:import/errors", 10 | "plugin:import/warnings", 11 | "plugin:import/typescript", 12 | "plugin:prettier/recommended" 13 | ], 14 | "plugins": ["@typescript-eslint"], 15 | "rules": { 16 | "@typescript-eslint/camelcase": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: peter-evans -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "friday" 8 | labels: 9 | - "dependencies" 10 | 11 | - package-ecosystem: "npm" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | day: "friday" 16 | ignore: 17 | - dependency-name: "*" 18 | update-types: ["version-update:semver-major"] 19 | labels: 20 | - "dependencies" 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/automerge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge Dependabot 2 | on: pull_request 3 | 4 | jobs: 5 | automerge: 6 | runs-on: ubuntu-latest 7 | if: github.actor == 'dependabot[bot]' 8 | steps: 9 | - uses: peter-evans/enable-pull-request-automerge@v3 10 | with: 11 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 12 | pull-request-number: ${{ github.event.pull_request.number }} 13 | merge-method: squash 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | paths-ignore: 6 | - 'README.md' 7 | - 'docs/**' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - 'README.md' 12 | - 'docs/**' 13 | 14 | permissions: 15 | issues: write 16 | pull-requests: write 17 | contents: write 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: 20.x 27 | cache: npm 28 | - run: npm ci 29 | - run: npm run build 30 | - run: npm run format-check 31 | - run: npm run lint 32 | - run: npm run test 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: dist 36 | path: dist 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: action.yml 40 | path: action.yml 41 | 42 | test: 43 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository 44 | needs: [build] 45 | runs-on: ubuntu-latest 46 | strategy: 47 | matrix: 48 | target: [built, committed] 49 | steps: 50 | - uses: actions/checkout@v4 51 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 52 | uses: actions/download-artifact@v4 53 | with: 54 | name: dist 55 | path: dist 56 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 57 | uses: actions/download-artifact@v4 58 | with: 59 | name: action.yml 60 | path: . 61 | 62 | - name: Test create issue from file 63 | id: ciff 64 | uses: ./ 65 | with: 66 | title: '[CI] test ${{ matrix.target }}' 67 | content-filepath: ./example-content/output.md 68 | labels: | 69 | report 70 | automated issue 71 | assignees: peter-evans 72 | 73 | - name: Test update issue from file 74 | uses: ./ 75 | with: 76 | issue-number: ${{ steps.ciff.outputs.issue-number }} 77 | title: '[CI] test ${{ matrix.target }}' 78 | content-filepath: ./example-content/output.md 79 | labels: | 80 | report 81 | automated issue 82 | assignees: peter-evans 83 | 84 | - name: Close Issue 85 | uses: peter-evans/close-issue@v3 86 | with: 87 | issue-number: ${{ steps.ciff.outputs.issue-number }} 88 | comment: '[CI] test ${{ matrix.target }}' 89 | 90 | package: 91 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 92 | needs: [test] 93 | runs-on: ubuntu-latest 94 | steps: 95 | - uses: actions/checkout@v4 96 | - run: rm -rf dist 97 | - uses: actions/download-artifact@v4 98 | with: 99 | name: dist 100 | path: dist 101 | - name: Create Pull Request 102 | uses: peter-evans/create-pull-request@v7 103 | with: 104 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 105 | commit-message: Update distribution 106 | title: Update distribution 107 | body: | 108 | - Updates the distribution for changes on `main` 109 | 110 | Auto-generated by [create-pull-request][1] 111 | 112 | [1]: https://github.com/peter-evans/create-pull-request 113 | branch: update-distribution 114 | -------------------------------------------------------------------------------- /.github/workflows/ciff-example-command.yml: -------------------------------------------------------------------------------- 1 | name: Create Issue From File Example Command 2 | on: 3 | repository_dispatch: 4 | types: [ciff-example-command] 5 | jobs: 6 | createIssueFromFile: 7 | runs-on: ubuntu-latest 8 | steps: 9 | # Get the target repository and branch 10 | - name: Get the target repository and branch 11 | id: vars 12 | run: | 13 | repository=${{ github.event.client_payload.slash_command.repository }} 14 | if [[ -z "$repository" ]]; then repository=${{ github.repository }}; fi 15 | echo "repository=$repository" >> $GITHUB_OUTPUT 16 | branch=${{ github.event.client_payload.slash_command.branch }} 17 | if [[ -z "$branch" ]]; then branch="main"; fi 18 | echo "branch=$branch" >> $GITHUB_OUTPUT 19 | 20 | # Checkout the branch to test 21 | - uses: actions/checkout@v4 22 | with: 23 | repository: ${{ steps.vars.outputs.repository }} 24 | ref: ${{ steps.vars.outputs.branch }} 25 | 26 | # Test create 27 | - name: Create Issue From File 28 | id: ciff 29 | uses: ./ 30 | with: 31 | title: An example issue 32 | content-filepath: ./example-content/output.md 33 | labels: | 34 | report 35 | automated issue 36 | assignees: retepsnave 37 | 38 | # Test update 39 | - name: Create Issue From File 40 | uses: ./ 41 | with: 42 | issue-number: ${{ steps.ciff.outputs.issue-number }} 43 | title: An example issue 44 | content-filepath: ./example-content/output.md 45 | labels: | 46 | report 47 | automated issue 48 | assignees: retepsnave 49 | 50 | - name: Add reaction 51 | uses: peter-evans/create-or-update-comment@v4 52 | with: 53 | repository: ${{ github.event.client_payload.github.payload.repository.full_name }} 54 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 55 | reaction-type: hooray 56 | -------------------------------------------------------------------------------- /.github/workflows/slash-command-dispatch.yml: -------------------------------------------------------------------------------- 1 | name: Slash Command Dispatch 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | slashCommandDispatch: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Slash Command Dispatch 10 | uses: peter-evans/slash-command-dispatch@v4 11 | with: 12 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 13 | config: > 14 | [ 15 | { 16 | "command": "rebase", 17 | "permission": "admin", 18 | "repository": "peter-evans/slash-command-dispatch-processor", 19 | "issue_type": "pull-request" 20 | }, 21 | { 22 | "command": "ciff-example", 23 | "permission": "admin", 24 | "named_args": true 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /.github/workflows/update-major-version.yml: -------------------------------------------------------------------------------- 1 | name: Update Major Version 2 | run-name: Update ${{ github.event.inputs.main_version }} to ${{ github.event.inputs.target }} 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | target: 8 | description: The target tag or reference 9 | required: true 10 | main_version: 11 | type: choice 12 | description: The major version tag to update 13 | options: 14 | - v5 15 | 16 | jobs: 17 | tag: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 23 | fetch-depth: 0 24 | - name: Git config 25 | run: | 26 | git config user.name actions-bot 27 | git config user.email actions-bot@users.noreply.github.com 28 | - name: Tag new target 29 | run: git tag -f ${{ github.event.inputs.main_version }} ${{ github.event.inputs.target }} 30 | - name: Push new tag 31 | run: git push origin ${{ github.event.inputs.main_version }} --force 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peter Evans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create Issue From File 2 | [![CI](https://github.com/peter-evans/create-issue-from-file/workflows/CI/badge.svg)](https://github.com/peter-evans/create-issue-from-file/actions?query=workflow%3ACI) 3 | [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Create%20Issue%20From%20File-blue.svg?colorA=24292e&colorB=0366d6&style=flat&longCache=true&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAM6wAADOsB5dZE0gAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAERSURBVCiRhZG/SsMxFEZPfsVJ61jbxaF0cRQRcRJ9hlYn30IHN/+9iquDCOIsblIrOjqKgy5aKoJQj4O3EEtbPwhJbr6Te28CmdSKeqzeqr0YbfVIrTBKakvtOl5dtTkK+v4HfA9PEyBFCY9AGVgCBLaBp1jPAyfAJ/AAdIEG0dNAiyP7+K1qIfMdonZic6+WJoBJvQlvuwDqcXadUuqPA1NKAlexbRTAIMvMOCjTbMwl1LtI/6KWJ5Q6rT6Ht1MA58AX8Apcqqt5r2qhrgAXQC3CZ6i1+KMd9TRu3MvA3aH/fFPnBodb6oe6HM8+lYHrGdRXW8M9bMZtPXUji69lmf5Cmamq7quNLFZXD9Rq7v0Bpc1o/tp0fisAAAAASUVORK5CYII=)](https://github.com/marketplace/actions/create-issue-from-file) 4 | 5 | A GitHub action to create an issue using content from a file. 6 | 7 | This is designed to be used in conjunction with other actions that output to a file. 8 | Especially if that output can be formatted as [GitHub flavoured Markdown](https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax). 9 | This action will create an issue if a file exists at a specified path. 10 | The content of the issue will be taken from the file as-is. 11 | If the file does not exist the action exits silently. 12 | 13 | ## Usage 14 | 15 | ```yml 16 | - name: Create Issue From File 17 | uses: peter-evans/create-issue-from-file@v5 18 | with: 19 | title: An example issue 20 | content-filepath: ./example-content/output.md 21 | labels: | 22 | report 23 | automated issue 24 | ``` 25 | 26 | ### Action inputs 27 | 28 | | Name | Description | Default | 29 | | --- | --- | --- | 30 | | `token` | `GITHUB_TOKEN` or a `repo` scoped [PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | `GITHUB_TOKEN` | 31 | | `repository` | The target GitHub repository | Current repository | 32 | | `issue-number` | The issue number of an existing issue to update | | 33 | | `title` | (**required**) The title of the issue | | 34 | | `content-filepath` | The file path to the issue content | | 35 | | `labels` | A comma or newline-separated list of labels | | 36 | | `assignees` | A comma or newline-separated list of assignees (GitHub usernames) | | 37 | 38 | ### Outputs 39 | 40 | - `issue-number` - The number of the created issue 41 | 42 | ## License 43 | 44 | MIT License - see the [LICENSE](LICENSE) file for details 45 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Create Issue From File' 2 | description: 'An action to create an issue using content from a file' 3 | inputs: 4 | token: 5 | description: 'The GitHub authentication token' 6 | default: ${{ github.token }} 7 | repository: 8 | description: 'The target GitHub repository' 9 | default: ${{ github.repository }} 10 | issue-number: 11 | description: 'The issue number of an existing issue to update' 12 | title: 13 | description: 'The title of the issue' 14 | required: true 15 | content-filepath: 16 | description: 'The file path to the issue content' 17 | labels: 18 | description: 'A comma or newline-separated list of labels' 19 | assignees: 20 | description: 'A comma or newline-separated list of assignees (GitHub usernames)' 21 | outputs: 22 | issue-number: 23 | description: 'The number of the created issue' 24 | runs: 25 | using: 'node20' 26 | main: 'dist/index.js' 27 | branding: 28 | icon: 'alert-circle' 29 | color: 'orange' 30 | -------------------------------------------------------------------------------- /example-content/output.md: -------------------------------------------------------------------------------- 1 | # title 2 | 3 | An example issue created using content from [example-content/output.md](https://github.com/peter-evans/create-issue-from-file/blob/main/example-content/output.md) 4 | 5 | ## subtitle 6 | 7 | Some text 8 | 9 | - bullet points 10 | - bullet points 11 | 12 | "quoted string" 13 | 14 | ```python 15 | s = "syntax highlighting" 16 | print(s) 17 | ``` 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-issue-from-file", 3 | "version": "5.0.0", 4 | "private": true, 5 | "description": "An action to create an issue using content from a file", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc && ncc build", 9 | "format": "prettier --write '**/*.ts'", 10 | "format-check": "prettier --check '**/*.ts'", 11 | "lint": "eslint src/**/*.ts", 12 | "test": "jest --passWithNoTests" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/peter-evans/create-issue-from-file.git" 17 | }, 18 | "keywords": [ 19 | "actions", 20 | "create", 21 | "issue" 22 | ], 23 | "author": "Peter Evans", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/peter-evans/create-issue-from-file/issues" 27 | }, 28 | "homepage": "https://github.com/peter-evans/create-issue-from-file", 29 | "dependencies": { 30 | "@actions/core": "^1.11.1", 31 | "@actions/github": "^6.0.1" 32 | }, 33 | "devDependencies": { 34 | "@types/jest": "^27.0.3", 35 | "@types/node": "^18.19.111", 36 | "@typescript-eslint/parser": "^5.62.0", 37 | "@vercel/ncc": "^0.38.3", 38 | "eslint": "^8.57.1", 39 | "eslint-plugin-github": "^4.10.2", 40 | "eslint-plugin-jest": "^27.9.0", 41 | "eslint-plugin-prettier": "^5.4.1", 42 | "jest": "^27.5.1", 43 | "jest-circus": "^29.7.0", 44 | "js-yaml": "^4.1.0", 45 | "prettier": "^3.5.3", 46 | "ts-jest": "^27.1.5", 47 | "typescript": "^4.9.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import * as fs from 'fs' 4 | import * as util from 'util' 5 | import * as utils from './utils' 6 | import {inspect} from 'util' 7 | 8 | function truncateBody(body: string) { 9 | // 65536 characters is the maximum allowed for issues. 10 | const truncateWarning = '...*[Issue body truncated]*' 11 | if (body.length > 65536) { 12 | core.warning(`Issue body is too long. Truncating to 65536 characters.`) 13 | return body.substring(0, 65536 - truncateWarning.length) + truncateWarning 14 | } 15 | return body 16 | } 17 | 18 | async function run(): Promise { 19 | try { 20 | const inputs = { 21 | token: core.getInput('token'), 22 | repository: core.getInput('repository'), 23 | issueNumber: Number(core.getInput('issue-number')), 24 | title: core.getInput('title'), 25 | contentFilepath: core.getInput('content-filepath'), 26 | labels: utils.getInputAsArray('labels'), 27 | assignees: utils.getInputAsArray('assignees') 28 | } 29 | core.debug(`Inputs: ${inspect(inputs)}`) 30 | 31 | const [owner, repo] = inputs.repository.split('/') 32 | core.debug(`Repo: ${inspect(repo)}`) 33 | 34 | const octokit = github.getOctokit(inputs.token) 35 | 36 | // Check the file exists 37 | if (await util.promisify(fs.exists)(inputs.contentFilepath)) { 38 | // Fetch the file content 39 | let fileContent = await fs.promises.readFile(inputs.contentFilepath, { 40 | encoding: 'utf8' 41 | }) 42 | 43 | fileContent = truncateBody(fileContent) 44 | 45 | const issueNumber = await (async (): Promise => { 46 | if (inputs.issueNumber) { 47 | // Update an existing issue 48 | await octokit.rest.issues.update({ 49 | owner: owner, 50 | repo: repo, 51 | issue_number: inputs.issueNumber, 52 | title: inputs.title, 53 | body: fileContent 54 | }) 55 | core.info(`Updated issue #${inputs.issueNumber}`) 56 | return inputs.issueNumber 57 | } else { 58 | // Create an issue 59 | const {data: issue} = await octokit.rest.issues.create({ 60 | owner: owner, 61 | repo: repo, 62 | title: inputs.title, 63 | body: fileContent 64 | }) 65 | core.info(`Created issue #${issue.number}`) 66 | return issue.number 67 | } 68 | })() 69 | 70 | // Apply labels 71 | if (inputs.labels.length > 0) { 72 | core.info(`Applying labels '${inputs.labels}'`) 73 | await octokit.rest.issues.addLabels({ 74 | owner: owner, 75 | repo: repo, 76 | issue_number: issueNumber, 77 | labels: inputs.labels 78 | }) 79 | } 80 | // Apply assignees 81 | if (inputs.assignees.length > 0) { 82 | core.info(`Applying assignees '${inputs.assignees}'`) 83 | await octokit.rest.issues.addAssignees({ 84 | owner: owner, 85 | repo: repo, 86 | issue_number: issueNumber, 87 | assignees: inputs.assignees 88 | }) 89 | } 90 | 91 | // Set output 92 | core.setOutput('issue-number', issueNumber) 93 | } else { 94 | core.info(`File not found at path '${inputs.contentFilepath}'`) 95 | } 96 | } catch (error) { 97 | core.debug(inspect(error)) 98 | core.setFailed(utils.getErrorMessage(error)) 99 | } 100 | } 101 | 102 | run() 103 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | export function getInputAsArray( 4 | name: string, 5 | options?: core.InputOptions 6 | ): string[] { 7 | return getStringAsArray(core.getInput(name, options)) 8 | } 9 | 10 | export function getStringAsArray(str: string): string[] { 11 | return str 12 | .split(/[\n,]+/) 13 | .map(s => s.trim()) 14 | .filter(x => x !== '') 15 | } 16 | 17 | export function getErrorMessage(error: unknown) { 18 | if (error instanceof Error) return error.message 19 | return String(error) 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "outDir": "./lib", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "esModuleInterop": true 14 | }, 15 | "exclude": ["__test__", "lib", "node_modules"] 16 | } 17 | --------------------------------------------------------------------------------