├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── continuous-delivery.yml │ ├── continuous-integration.yml │ └── linter.yml ├── .gitignore ├── .markdown-lint.yml ├── .mega-linter.yml ├── .node-version ├── .prettierignore ├── .prettierrc.yml ├── .vscode ├── settings.json └── tasks.json ├── .yaml-lint.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── components.json ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── src ├── app │ ├── favicon.ico │ ├── globals.css │ ├── introduction │ │ ├── best-practices │ │ │ └── page.tsx │ │ ├── issues-and-prs │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── workflow-security │ │ │ └── page.tsx │ ├── layout.tsx │ ├── not-found.tsx │ ├── page.tsx │ ├── reference │ │ ├── branch-deployments │ │ │ └── page.tsx │ │ ├── examples │ │ │ └── page.tsx │ │ └── issueops-actions │ │ │ └── page.tsx │ ├── setup │ │ ├── comment-workflow │ │ │ └── page.tsx │ │ ├── github-app │ │ │ └── page.tsx │ │ ├── issue-form │ │ │ └── page.tsx │ │ ├── issue-workflow │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── repository │ │ │ └── page.tsx │ └── states-and-transitions │ │ ├── approve │ │ └── page.tsx │ │ ├── deny │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── parse │ │ └── page.tsx │ │ ├── submit │ │ └── page.tsx │ │ └── validate │ │ └── page.tsx ├── components │ ├── app-sidebar.tsx │ ├── nav-footer.tsx │ ├── nav-main.tsx │ ├── nav-projects.tsx │ ├── nav-user.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── collapsible.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── hooks │ └── use-mobile.tsx ├── lib │ └── utils.ts └── public │ ├── .nojekyll │ └── img │ ├── icon.png │ ├── octocat.png │ └── supportcat.png └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: monthly 8 | labels: 9 | - dependabot 10 | - actions 11 | open-pull-requests-limit: 10 12 | reviewers: 13 | - issue-ops/maintainers 14 | groups: 15 | actions-minor: 16 | update-types: 17 | - minor 18 | - patch 19 | 20 | - package-ecosystem: npm 21 | directory: / 22 | schedule: 23 | interval: monthly 24 | labels: 25 | - dependabot 26 | - npm 27 | open-pull-requests-limit: 10 28 | reviewers: 29 | - issue-ops/maintainers 30 | groups: 31 | npm-development: 32 | dependency-type: development 33 | update-types: 34 | - minor 35 | - patch 36 | npm-production: 37 | dependency-type: production 38 | update-types: 39 | - patch 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '30 1 * * 4' 12 | 13 | permissions: 14 | actions: read 15 | checks: write 16 | contents: read 17 | security-events: write 18 | 19 | jobs: 20 | analyze: 21 | name: Analyze 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | language: 28 | - javascript 29 | 30 | steps: 31 | - name: Checkout 32 | id: checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Initialize CodeQL 36 | id: initialize 37 | uses: github/codeql-action/init@v3 38 | with: 39 | languages: ${{ matrix.language }} 40 | 41 | - name: Autobuild 42 | id: autobuild 43 | uses: github/codeql-action/autobuild@v3 44 | 45 | - name: Perform CodeQL Analysis 46 | id: analyze 47 | uses: github/codeql-action/analyze@v3 48 | -------------------------------------------------------------------------------- /.github/workflows/continuous-delivery.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Delivery 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: pages 13 | cancel-in-progress: false 14 | 15 | permissions: 16 | contents: write 17 | id-token: write 18 | pages: write 19 | 20 | jobs: 21 | build: 22 | name: Build Site 23 | runs-on: ubuntu-latest 24 | 25 | # Only run this job if it was done manually or the PR was merged 26 | if: | 27 | (github.event_name == 'workflow_dispatch' || 28 | github.event.pull_request.merged == true) && 29 | github.actor != 'dependabot[bot]' 30 | 31 | steps: 32 | - name: Checkout 33 | id: checkout 34 | uses: actions/checkout@v4 35 | 36 | - name: Setup Node.js 37 | id: setup-node 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version-file: .node-version 41 | cache: npm 42 | 43 | - name: Setup Pages 44 | id: pages 45 | uses: actions/configure-pages@v5 46 | 47 | # prettier-ignore 48 | - name: Restore Cache 49 | id: cache 50 | uses: actions/cache@v4 51 | with: 52 | path: .next/cache 53 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 54 | restore-keys: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- 55 | 56 | - name: Install Dependencies 57 | id: install 58 | run: npm ci 59 | 60 | - name: Build 61 | id: build 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | run: npm run build 65 | 66 | - name: Upload Pages Artifact 67 | id: upload 68 | uses: actions/upload-pages-artifact@v3 69 | with: 70 | path: ./out 71 | 72 | deploy: 73 | name: Deploy to GitHub Pages 74 | runs-on: ubuntu-latest 75 | 76 | needs: build 77 | 78 | environment: 79 | name: github-pages 80 | url: ${{ steps.deploy.outputs.page_url }} 81 | 82 | steps: 83 | - name: Deploy to GitHub Pages 84 | id: deploy 85 | uses: actions/deploy-pages@v4 86 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | checks: write 13 | contents: read 14 | 15 | jobs: 16 | continuous-integration: 17 | name: Continuous Integration 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout 22 | id: checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Node.js 26 | id: setup-node 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version-file: .node-version 30 | cache: npm 31 | 32 | - name: Install Dependencies 33 | id: install 34 | run: npm install 35 | 36 | - name: Check Format 37 | id: format-check 38 | run: npm run format:check 39 | 40 | - name: Lint 41 | id: lint 42 | run: npm run lint 43 | 44 | - name: Build 45 | id: build 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | run: npm run build 49 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Lint Codebase 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: read 13 | issues: write 14 | packages: read 15 | pull-requests: write 16 | statuses: write 17 | 18 | jobs: 19 | lint: 20 | name: Lint Codebase 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout 25 | id: checkout 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Setup Node.js 31 | id: setup-node 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version-file: .node-version 35 | cache: npm 36 | 37 | - name: Install Dependencies 38 | id: install 39 | run: npm ci 40 | 41 | - name: Lint Codebase 42 | id: lint 43 | uses: oxsecurity/megalinter/flavors/javascript@v8 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | MD003: false 2 | MD004: 3 | style: dash 4 | MD013: 5 | tables: false 6 | MD026: false 7 | MD029: 8 | style: one 9 | MD033: false 10 | MD034: false 11 | MD036: false 12 | MD041: false 13 | -------------------------------------------------------------------------------- /.mega-linter.yml: -------------------------------------------------------------------------------- 1 | # Mega-Linter Configuration File 2 | # https://megalinter.io/latest/config-file/ 3 | 4 | # Activates formatting and autofix 5 | APPLY_FIXES: none 6 | 7 | # Flag to clear files from report folder before starting the linting process 8 | CLEAR_REPORT_FOLDER: true 9 | 10 | DEFAULT_BRANCH: main 11 | 12 | # List of disabled descriptors keys 13 | # https://megalinter.io/latest/config-activation/ 14 | DISABLE: 15 | - COPYPASTE 16 | - JAVASCRIPT 17 | - PYTHON 18 | - SPELL 19 | 20 | # List of disabled linters keys 21 | # https://megalinter.io/latest/config-activation/ 22 | DISABLE_LINTERS: 23 | - CSS_STYLELINT 24 | - JSON_NPM_PACKAGE_JSON_LINT 25 | - MARKDOWN_MARKDOWN_TABLE_FORMATTER 26 | - REPOSITORY_GRYPE 27 | - REPOSITORY_KICS 28 | - REPOSITORY_TRIVY 29 | - REPOSITORY_TRUFFLEHOG 30 | - TYPESCRIPT_STANDARD 31 | 32 | # List of enabled but not blocking linters keys 33 | # https://megalinter.io/latest/config-activation/ 34 | # DISABLE_ERRORS_LINTERS: [] 35 | 36 | # List of enabled descriptors keys 37 | # https://megalinter.io/latest/config-activation/ 38 | # If you use ENABLE variable, all other linters will be disabled by default 39 | # ENABLE: [] 40 | 41 | # List of enabled linters keys 42 | # If you use ENABLE_LINTERS variable, all other linters will be disabled 43 | # ENABLE_LINTERS: [] 44 | 45 | # List of excluded directory basenames. 46 | EXCLUDED_DIRECTORIES: 47 | - .cache 48 | - .git 49 | - coverage 50 | - dist 51 | - megalinter-reports 52 | - node_modules 53 | - public 54 | - scripts 55 | - reports 56 | 57 | # If set to true, MegaLinter fails if a linter or formatter has autofixed 58 | # sources, even if there are no errors 59 | FAIL_IF_UPDATED_SOURCES: false 60 | 61 | # Upload reports to file.io 62 | FILEIO_REPORTER: false 63 | 64 | # Provides suggestions about different MegaLinter flavors to use to improve 65 | # runtime performance 66 | FLAVOR_SUGGESTIONS: true 67 | 68 | # Formatter errors will be reported as errors (and not warnings) if this 69 | # variable is set to false 70 | FORMATTERS_DISABLE_ERRORS: false 71 | 72 | # Posts a comment on the pull request with linting results 73 | GITHUB_COMMENT_REPORTER: true 74 | 75 | # Sets pull request status checks on GitHub 76 | GITHUB_STATUS_REPORTER: true 77 | 78 | # If set to true, MegaLinter will skip files containing @generated marker but 79 | # without @not-generated marker (more info at https://generated.at) 80 | IGNORE_GENERATED_FILES: true 81 | 82 | # If set to true, MegaLinter will skip files ignored by git using .gitignore 83 | IGNORE_GITIGNORED_FILES: true 84 | 85 | # JavaScript default style to check/apply 86 | JAVASCRIPT_DEFAULT_STYLE: prettier 87 | 88 | # Directory for all linter configuration rules 89 | # Can be a local folder or a remote URL 90 | # (ex: https://raw.githubusercontent.com/some_org/some_repo/mega-linter-rules) 91 | LINTER_RULES_PATH: . 92 | 93 | # The file name for outputting logs. All output is sent to the log file 94 | # regardless of LOG_LEVEL 95 | LOG_FILE: linter.log 96 | 97 | # How much output the script will generate to the console. One of INFO, DEBUG, 98 | # WARNING or ERROR. 99 | LOG_LEVEL: INFO 100 | 101 | # Markdown default style to check/apply 102 | MARKDOWN_DEFAULT_STYLE: markdownlint 103 | 104 | MARKDOWN_MARKDOWNLINT_FILTER_REGEX_EXCLUDE: __fixtures__ 105 | 106 | # Generate Markdown summary report 107 | MARKDOWN_SUMMARY_REPORTER: true 108 | 109 | # Name of the Markdown summary report file 110 | MARKDOWN_SUMMARY_REPORTER_FILE_NAME: summary.md 111 | 112 | # Process linters in parallel to improve overall MegaLinter performance. If 113 | # true, linters of same language or formats are grouped in the same parallel 114 | # process to avoid lock issues if fixing the same files 115 | PARALLEL: true 116 | 117 | # All available cores are used by default. If there are too many, you need to 118 | # decrease the number of used cores in order to enhance performance 119 | # PARALLEL_PROCESS_NUMBER: 4 120 | 121 | # Directory for generating report files 122 | # Set to none to not generate reports 123 | REPORT_OUTPUT_FOLDER: linter 124 | 125 | # Set to simple to avoid external images in generated markdown 126 | REPORTERS_MARKDOWN_TYPE: advanced 127 | 128 | # Additional list of secured environment variables to hide when calling linters. 129 | # SECURED_ENV_VARIABLES: [] 130 | 131 | # Displays elapsed time in reports 132 | SHOW_ELAPSED_TIME: true 133 | 134 | # Displays all disabled linters mega-linter could have run 135 | SHOW_SKIPPED_LINTERS: false 136 | 137 | # Typescript default style to check/apply 138 | TYPESCRIPT_DEFAULT_STYLE: prettier 139 | 140 | # Will parse the entire repository and find all files to validate 141 | # When set to false, only new or edited files will be parsed for validation 142 | VALIDATE_ALL_CODEBASE: true 143 | 144 | # Per-linter configuration 145 | JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.yml 146 | JAVASCRIPT_PRETTIER_CONFIG_FILE: prettierrc.yml 147 | JSON_PRETTIER_CONFIG_FILE: prettierrc.yml 148 | MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.yml 149 | TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.yml 150 | TYPESCRIPT_PRETTIER_CONFIG_FILE: .prettierrc.yml 151 | YAML_PRETTIER_CONFIG_FILE: .prettierrc.yml 152 | YAML_YAMLLINT_CONFIG_FILE: .yaml-lint.yml 153 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 22.9.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | .DS_Store 3 | .bundle/ 4 | .jekyll* 5 | .sass-cache/ 6 | _site/ 7 | node_modules/ 8 | *.min.js 9 | *.min.js.map 10 | *.scss 11 | dist/ 12 | coverage/ 13 | fixtures/ 14 | .gitattributes 15 | .gitignore 16 | .node-version 17 | .prettierignore 18 | CODEOWNERS 19 | LICENSE 20 | favicon.ico 21 | .nojekyll 22 | *.png -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | printWidth: 80 2 | tabWidth: 2 3 | useTabs: false 4 | semi: false 5 | singleQuote: true 6 | quoteProps: as-needed 7 | jsxSingleQuote: false 8 | trailingComma: none 9 | bracketSpacing: true 10 | bracketSameLine: true 11 | arrowParens: always 12 | proseWrap: always 13 | htmlWhitespaceSensitivity: css 14 | endOfLine: lf 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.inlineSuggest.enabled": true, 3 | "editor.rulers": [80], 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.formatOnSave": true, 6 | "editor.tabSize": 2, 7 | "editor.codeActionsOnSave": { 8 | "source.organizeImports": "always" 9 | }, 10 | "html.format.templating": true, 11 | "markdown.extension.list.indentationSize": "adaptive", 12 | "markdown.extension.italic.indicator": "_", 13 | "markdown.extension.orderedList.marker": "one", 14 | "[html]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[xml]": { 18 | "editor.defaultFormatter": "redhat.vscode-xml" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Workspace: Lint", 6 | "type": "shell", 7 | "group": "test", 8 | "command": "npx mega-linter-runner --flavor cupcake", 9 | "options": { 10 | "cwd": "${workspaceFolder}" 11 | }, 12 | "problemMatcher": [] 13 | }, 14 | { 15 | "label": "npm: Test", 16 | "type": "shell", 17 | "group": "test", 18 | "command": "npm test", 19 | "options": { 20 | "cwd": "${workspaceFolder}" 21 | } 22 | }, 23 | { 24 | "label": "npm: Lint", 25 | "type": "shell", 26 | "group": "test", 27 | "command": "npm run lint", 28 | "options": { 29 | "cwd": "${workspaceFolder}" 30 | } 31 | }, 32 | { 33 | "label": "npm: Format (Check)", 34 | "group": "test", 35 | "type": "shell", 36 | "command": "npm run format:check", 37 | "options": { 38 | "cwd": "${workspaceFolder}" 39 | } 40 | }, 41 | { 42 | "label": "npm: Format (Write)", 43 | "group": "test", 44 | "type": "shell", 45 | "command": "npm run format:write", 46 | "options": { 47 | "cwd": "${workspaceFolder}" 48 | } 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.yaml-lint.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | document-end: disable 3 | document-start: 4 | level: warning 5 | present: false 6 | line-length: 7 | level: warning 8 | max: 120 9 | allow-non-breakable-words: true 10 | allow-non-breakable-inline-mappings: true 11 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Repository CODEOWNERS 2 | 3 | * @issue-ops/maintainers 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and 9 | expression, level of experience, education, socio-economic status, nationality, 10 | personal appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project email address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at 59 | [opensource@github.com](mailto:opensource@github.com). All complaints will be 60 | reviewed and investigated and will result in a response that is deemed necessary 61 | and appropriate to the circumstances. The project team is obligated to maintain 62 | confidentiality with regard to the reporter of an incident. Further details of 63 | specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the 72 | [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, 73 | available at 74 | [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 75 | 76 | For answers to common questions about this code of conduct, see 77 | [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are welcome and greatly appreciated! 4 | 5 | ## Steps to Contribute 6 | 7 | > [!WARNING] 8 | > 9 | > Check the `engine` property in [`package.json`](./package.json) to see what 10 | > version of Node.js is required for local development. This can be different 11 | > from the version of Node.js used on the GitHub Actions runners. Tools like 12 | > [nodenv](https://github.com/nodenv/nodenv) can be used to manage your Node.js 13 | > version automatically. 14 | 15 | 1. Fork this repository 16 | 1. Work on your changes 17 | 1. Preview your changes (`npm run develop`) 18 | 1. Open a pull request back to this repository 19 | 1. Notify the maintainers of this repository for peer review and approval 20 | 1. Merge :tada: 21 | 22 | The maintainers of this repository will review your changes and provide any 23 | feedback. Once approved, they will be merged in and a new version of the site 24 | will be deployed. You'll also be able to see your GitHub profile tagged in the 25 | contributors list for any pages you contribute to! 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) GitHub 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 | # IssueOps Documentation 2 | 3 | Issue the ops, and ops the docs! 4 | 5 | ![Continuous Integration](https://github.com/issue-ops/docs/actions/workflows/continuous-integration.yml/badge.svg) 6 | ![Continuous Delivery](https://github.com/issue-ops/docs/actions/workflows/continuous-delivery.yml/badge.svg) 7 | ![Linter](https://github.com/issue-ops/docs/actions/workflows/linter.yml/badge.svg) 8 | 9 | This repository hosts the 10 | [IssueOps documentation page](https://issue-ops.github.io/docs) and everything 11 | you need to get started building your own awesome IssueOps workflows! 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | Thanks for helping make GitHub safe for everyone! 4 | 5 | GitHub takes the security of our software products and services seriously, 6 | including all of the open source code repositories managed through our GitHub 7 | organizations, such as [GitHub](https://github.com/GitHub). 8 | 9 | Even though 10 | [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) 11 | and therefore not eligible for bounty rewards, we will ensure that your finding 12 | gets passed along to the appropriate maintainers for remediation. 13 | 14 | ## Reporting Security Issues 15 | 16 | If you believe you have found a security vulnerability in any GitHub-owned 17 | repository, please report it to us through coordinated disclosure. 18 | 19 | **Please do not report security vulnerabilities through public GitHub issues, 20 | discussions, or pull requests.** 21 | 22 | Instead, please send an email to 23 | [opensource-security@github.com](mailto:opensource-security@github.com). 24 | 25 | Please include as much of the information listed below as you can to help us 26 | better understand and resolve the issue: 27 | 28 | - The type of issue (e.g., buffer overflow, SQL injection, or cross-site 29 | scripting) 30 | - Full paths of source file(s) related to the manifestation of the issue 31 | - The location of the affected source code (tag/branch/commit or direct URL) 32 | - Any special configuration required to reproduce the issue 33 | - Step-by-step instructions to reproduce the issue 34 | - Proof-of-concept or exploit code (if possible) 35 | - Impact of the issue, including how an attacker might exploit the issue 36 | 37 | This information will help us triage your report more quickly. 38 | 39 | ## Policy 40 | 41 | See 42 | [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms) 43 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub issues to track bugs and feature requests. Please 6 | search the existing issues before filing new issues to avoid duplicates. For new 7 | issues, file your bug or feature request as a new issue. 8 | 9 | For help or questions about using this project, please feel free to submit an 10 | issue as well. 11 | 12 | This project is under active development and maintained by GitHub staff and the 13 | community. We will do our best to respond to support, feature requests, and 14 | community questions in a timely manner. 15 | 16 | ## GitHub Support Policy 17 | 18 | Support for this project is limited to the resources listed above. 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc' 2 | import { dirname } from 'path' 3 | import { fileURLToPath } from 'url' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = dirname(__filename) 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname 10 | }) 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript') 14 | ] 15 | 16 | export default eslintConfig 17 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | output: 'export', 5 | basePath: '/docs', 6 | images: { 7 | unoptimized: true 8 | } 9 | } 10 | 11 | export default nextConfig 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "issue-ops-docs", 3 | "description": "IssueOps Documentation", 4 | "author": "Nick Alteen ", 5 | "version": "1.0.1", 6 | "homepage": "https://github.com/issue-ops/docs#readme", 7 | "repository": "issue-ops/docs", 8 | "bugs": { 9 | "url": "https://github.com/issue-ops/docs/issues" 10 | }, 11 | "keywords": [ 12 | "GitHub", 13 | "IssueOps" 14 | ], 15 | "private": true, 16 | "engines": { 17 | "node": ">=20" 18 | }, 19 | "scripts": { 20 | "dev": "next dev --turbopack", 21 | "format:check": "prettier --check '**'", 22 | "format:write": "prettier --write '**'", 23 | "build": "next build", 24 | "start": "next start", 25 | "lint": "next lint" 26 | }, 27 | "license": "MIT", 28 | "dependencies": { 29 | "@emotion/react": "^11.14.0", 30 | "@emotion/styled": "^11.14.0", 31 | "@hookform/resolvers": "^5.0.1", 32 | "@lightenna/react-mermaid-diagram": "^1.0.20", 33 | "@mui/material": "^7.1.1", 34 | "@primer/octicons-react": "^19.15.2", 35 | "@primer/react": "^37.25.0", 36 | "@radix-ui/react-avatar": "^1.1.10", 37 | "@radix-ui/react-collapsible": "^1.1.11", 38 | "@radix-ui/react-dialog": "^1.1.14", 39 | "@radix-ui/react-dropdown-menu": "^2.1.15", 40 | "@radix-ui/react-label": "^2.1.7", 41 | "@radix-ui/react-select": "^2.2.5", 42 | "@radix-ui/react-separator": "^1.1.7", 43 | "@radix-ui/react-slot": "^1.2.3", 44 | "@radix-ui/react-tabs": "^1.1.12", 45 | "@radix-ui/react-tooltip": "^1.2.7", 46 | "class-variance-authority": "^0.7.1", 47 | "clsx": "^2.1.1", 48 | "embla-carousel-react": "^8.6.0", 49 | "lucide-react": "^0.513.0", 50 | "mermaid": "^11.6.0", 51 | "next": "15.3.3", 52 | "next-themes": "^0.4.6", 53 | "react": "^19.0.0", 54 | "react-dom": "^19.1.0", 55 | "react-hook-form": "^7.56.1", 56 | "react-syntax-highlighter": "^15.6.1", 57 | "tailwind-merge": "^3.3.0", 58 | "tailwindcss-animate": "^1.0.7", 59 | "ts-dedent": "^2.2.0", 60 | "zod": "^3.25.46" 61 | }, 62 | "devDependencies": { 63 | "@eslint/eslintrc": "^3", 64 | "@tailwindcss/postcss": "^4.1.8", 65 | "@types/node": "^22", 66 | "@types/react": "^19", 67 | "@types/react-dom": "^19", 68 | "@types/react-syntax-highlighter": "^15.5.13", 69 | "eslint": "^9", 70 | "eslint-config-next": "15.3.3", 71 | "postcss": "^8", 72 | "prettier": "^3.5.3", 73 | "tailwindcss": "^4.0.12", 74 | "typescript": "^5" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {} 5 | } 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/issue-ops/docs/5ebbb95e26f3a3afa413cd820c95272d23976428/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @plugin 'tailwindcss-animate'; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | @theme { 8 | --color-background: hsl(var(--background)); 9 | --color-foreground: hsl(var(--foreground)); 10 | 11 | --color-card: hsl(var(--card)); 12 | --color-card-foreground: hsl(var(--card-foreground)); 13 | 14 | --color-popover: hsl(var(--popover)); 15 | --color-popover-foreground: hsl(var(--popover-foreground)); 16 | 17 | --color-primary: hsl(var(--primary)); 18 | --color-primary-foreground: hsl(var(--primary-foreground)); 19 | 20 | --color-secondary: hsl(var(--secondary)); 21 | --color-secondary-foreground: hsl(var(--secondary-foreground)); 22 | 23 | --color-muted: hsl(var(--muted)); 24 | --color-muted-foreground: hsl(var(--muted-foreground)); 25 | 26 | --color-accent: hsl(var(--accent)); 27 | --color-accent-foreground: hsl(var(--accent-foreground)); 28 | 29 | --color-destructive: hsl(var(--destructive)); 30 | --color-destructive-foreground: hsl(var(--destructive-foreground)); 31 | 32 | --color-border: hsl(var(--border)); 33 | --color-input: hsl(var(--input)); 34 | --color-ring: hsl(var(--ring)); 35 | 36 | --color-chart-1: hsl(var(--chart-1)); 37 | --color-chart-2: hsl(var(--chart-2)); 38 | --color-chart-3: hsl(var(--chart-3)); 39 | --color-chart-4: hsl(var(--chart-4)); 40 | --color-chart-5: hsl(var(--chart-5)); 41 | 42 | --color-sidebar: hsl(var(--sidebar-background)); 43 | --color-sidebar-foreground: hsl(var(--sidebar-foreground)); 44 | --color-sidebar-primary: hsl(var(--sidebar-primary)); 45 | --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground)); 46 | --color-sidebar-accent: hsl(var(--sidebar-accent)); 47 | --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground)); 48 | --color-sidebar-border: hsl(var(--sidebar-border)); 49 | --color-sidebar-ring: hsl(var(--sidebar-ring)); 50 | 51 | --radius-lg: var(--radius); 52 | --radius-md: calc(var(--radius) - 2px); 53 | --radius-sm: calc(var(--radius) - 4px); 54 | } 55 | 56 | /* 57 | The default border color has changed to `currentColor` in Tailwind CSS v4, 58 | so we've added these compatibility styles to make sure everything still 59 | looks the same as it did with Tailwind CSS v3. 60 | 61 | If we ever want to remove these styles, we need to add an explicit border 62 | color utility to any element that depends on these defaults. 63 | */ 64 | @layer base { 65 | *, 66 | ::after, 67 | ::before, 68 | ::backdrop, 69 | ::file-selector-button { 70 | border-color: var(--color-gray-200, currentColor); 71 | } 72 | } 73 | 74 | @layer utilities { 75 | body { 76 | font-family: Arial, Helvetica, sans-serif; 77 | } 78 | 79 | code { 80 | color: rgb(0, 255, 0); 81 | } 82 | } 83 | 84 | @layer base { 85 | :root { 86 | --background: 0 0% 100%; 87 | --foreground: 222.2 84% 4.9%; 88 | --card: 0 0% 100%; 89 | --card-foreground: 222.2 84% 4.9%; 90 | --popover: 0 0% 100%; 91 | --popover-foreground: 222.2 84% 4.9%; 92 | --primary: 222.2 47.4% 11.2%; 93 | --primary-foreground: 210 40% 98%; 94 | --secondary: 210 40% 96.1%; 95 | --secondary-foreground: 222.2 47.4% 11.2%; 96 | --muted: 210 40% 96.1%; 97 | --muted-foreground: 215.4 16.3% 46.9%; 98 | --accent: 210 40% 96.1%; 99 | --accent-foreground: 222.2 47.4% 11.2%; 100 | --destructive: 0 84.2% 60.2%; 101 | --destructive-foreground: 210 40% 98%; 102 | --border: 214.3 31.8% 91.4%; 103 | --input: 214.3 31.8% 91.4%; 104 | --ring: 222.2 84% 4.9%; 105 | --chart-1: 12 76% 61%; 106 | --chart-2: 173 58% 39%; 107 | --chart-3: 197 37% 24%; 108 | --chart-4: 43 74% 66%; 109 | --chart-5: 27 87% 67%; 110 | --radius: 0.5rem; 111 | --sidebar-background: 0 0% 98%; 112 | --sidebar-foreground: 240 5.3% 26.1%; 113 | --sidebar-primary: 240 5.9% 10%; 114 | --sidebar-primary-foreground: 0 0% 98%; 115 | --sidebar-accent: 240 4.8% 95.9%; 116 | --sidebar-accent-foreground: 240 5.9% 10%; 117 | --sidebar-border: 220 13% 91%; 118 | --sidebar-ring: 217.2 91.2% 59.8%; 119 | } 120 | .dark { 121 | --background: 222.2 84% 4.9%; 122 | --foreground: 210 40% 98%; 123 | --card: 222.2 84% 4.9%; 124 | --card-foreground: 210 40% 98%; 125 | --popover: 222.2 84% 4.9%; 126 | --popover-foreground: 210 40% 98%; 127 | --primary: 210 40% 98%; 128 | --primary-foreground: 222.2 47.4% 11.2%; 129 | --secondary: 217.2 32.6% 17.5%; 130 | --secondary-foreground: 210 40% 98%; 131 | --muted: 217.2 32.6% 17.5%; 132 | --muted-foreground: 215 20.2% 65.1%; 133 | --accent: 217.2 32.6% 17.5%; 134 | --accent-foreground: 210 40% 98%; 135 | --destructive: 0 62.8% 30.6%; 136 | --destructive-foreground: 210 40% 98%; 137 | --border: 217.2 32.6% 17.5%; 138 | --input: 217.2 32.6% 17.5%; 139 | --ring: 212.7 26.8% 83.9%; 140 | --chart-1: 220 70% 50%; 141 | --chart-2: 160 60% 45%; 142 | --chart-3: 30 80% 55%; 143 | --chart-4: 280 65% 60%; 144 | --chart-5: 340 75% 55%; 145 | --sidebar-background: 240 5.9% 10%; 146 | --sidebar-foreground: 240 4.8% 95.9%; 147 | --sidebar-primary: 224.3 76.3% 48%; 148 | --sidebar-primary-foreground: 0 0% 100%; 149 | --sidebar-accent: 240 3.7% 15.9%; 150 | --sidebar-accent-foreground: 240 4.8% 95.9%; 151 | --sidebar-border: 240 3.7% 15.9%; 152 | --sidebar-ring: 217.2 91.2% 59.8%; 153 | } 154 | } 155 | 156 | @layer base { 157 | * { 158 | @apply border-border; 159 | } 160 | body { 161 | @apply bg-background text-foreground; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/app/introduction/best-practices/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Card, CardDescription, CardHeader } from '@/components/ui/card' 4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' 5 | 6 | export default function Home() { 7 | return ( 8 |
9 |

Best Practices

10 | 11 |

GitHub APIs

12 | 13 | 14 | 15 | Do 16 | Don't 17 | 18 | 19 | 20 | 21 | 22 | Use GitHub Apps for accessing organization-level APIs 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Use personal access tokens 31 | 32 | 33 | 34 | 35 | 36 |

Sensitive information

37 | 38 | 39 | 40 | Do 41 | Don't 42 | 43 | 44 | 45 | 46 | 47 | Use issue forms inputs that accept references to sensitive 48 | information in secure locations 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Accept sensitive information directly in issues 58 | 59 | 60 | 61 | 62 | 63 | 64 |

Validation

65 | 66 | 67 | 68 | Do 69 | Don't 70 | 71 | 72 | 73 | 74 | 75 | Validate issue and comment text at every step in the IssueOps 76 | workflow 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | Rely on labels to determine if an issue has been validated or 86 | approved 87 | 88 | 89 | 90 | 91 | 92 |
93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /src/app/introduction/workflow-security/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 5 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' 6 | import dedent from 'ts-dedent' 7 | 8 | export default function Home() { 9 | return ( 10 |
11 |

Workflow Security

12 | 13 |

14 | The IssueOps model makes heavy use of the issue and{' '} 15 | issue_comment triggers in GitHub Actions workflows. 16 |

17 | 18 |
19 | 20 | {dedent` 21 | on: 22 | issue_comment: 23 | types: 24 | - created 25 | `} 26 | 27 |
28 | 29 |

30 | These triggers will only act on workflow files in the default{' '} 31 | branch of your repository. This means that pull requests cannot 32 | introduce changes to your IssueOps workflows that would be run as part 33 | of that PR (e.g. creating a workflow that dumps secrets to the logs). 34 | Any changes to the workflow files can be protected with branch 35 | protection rules to ensure only verified changes make it into your 36 | default branch. 37 |

38 | 39 |

Workflow permissions

40 | 41 |

42 | To further harden your workflow files, you should always restrict them 43 | to the base permissions needed to run. For information about the 44 | available permissions, see{' '} 45 | 48 | Assigning permissions to jobs 49 | 50 | . 51 |

52 | 53 |

54 | Permissions can be assigned for the entire workflow, as well as for 55 | individual jobs. If one job needs additional permissions, make sure to 56 | scope them to that job only. 57 |

58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AppSidebar } from '@/components/app-sidebar' 2 | import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar' 3 | import type { Metadata } from 'next' 4 | import { Geist, Geist_Mono } from 'next/font/google' 5 | import './globals.css' 6 | 7 | const geistSans = Geist({ 8 | variable: '--font-geist-sans', 9 | subsets: ['latin'] 10 | }) 11 | 12 | const geistMono = Geist_Mono({ 13 | variable: '--font-geist-mono', 14 | subsets: ['latin'] 15 | }) 16 | 17 | export const metadata: Metadata = { 18 | title: 'IssueOps Docs', 19 | description: 'A one-stop shop for all things IssueOps' 20 | } 21 | 22 | export default function RootLayout({ 23 | children 24 | }: Readonly<{ 25 | children: React.ReactNode 26 | }>) { 27 | return ( 28 | 29 | 31 | 32 | 33 | 34 |
35 |
36 | {children} 37 |
38 |
39 |
40 |
41 | 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Image from 'next/image' 4 | import supportcat from '../public/img/supportcat.png' 5 | 6 | export default function Home() { 7 | return ( 8 |
9 |

404 - Page Not Found

10 | 11 | Supportcat 12 | 13 |

14 | Uh oh...this page doesn't exist! 15 |

16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' 4 | import { Card, CardContent, CardHeader } from '@/components/ui/card' 5 | import { 6 | Carousel, 7 | CarouselContent, 8 | CarouselItem, 9 | CarouselNext, 10 | CarouselPrevious 11 | } from '@/components/ui/carousel' 12 | import { 13 | LockIcon, 14 | PencilIcon, 15 | SearchIcon, 16 | ZapIcon 17 | } from '@primer/octicons-react' 18 | import { Construction, Terminal } from 'lucide-react' 19 | import Link from 'next/link' 20 | 21 | const carouselItems = [ 22 | { 23 | icon: ZapIcon, 24 | title: 'Event-driven', 25 | description: 26 | 'Any time a user interacts with an issue or PR, an event is triggered. These events can be used to trigger GitHub Actions workflows.' 27 | }, 28 | { 29 | icon: PencilIcon, 30 | title: 'Customizable', 31 | description: 32 | 'Based on the event type and data provided, you can implement custom logic to perform virtually any task. If you can interact with it via an API, command-line tool, or script, you can probably build it with IssueOps.' 33 | }, 34 | { 35 | icon: SearchIcon, 36 | title: 'Transparent', 37 | description: 38 | 'All actions taken on an issue are recorded in the issue timeline.' 39 | }, 40 | { 41 | icon: LockIcon, 42 | title: 'Immutable', 43 | description: 44 | 'An issue or pull request creates an immutable record of the transaction, approvals, and actions that are taken. This follows GitHub\'s "everything has a URL" philosophy.' 45 | } 46 | ] 47 | 48 | export default function Home() { 49 | return ( 50 |
51 |

52 | The one stop shop for all things IssueOps 53 |

54 | 55 |

56 | If you landed on this page, you're probably trying to find the 57 | answer to the question What is IssueOps? If so, you came to the 58 | right place! The goal of this site is to provide education, best 59 | practices, examples, and resources for building IssueOps workflows on 60 | GitHub. 61 |

62 | 63 | 64 | 65 | Under Construction 66 | 67 | This site is a work in progress. If you have any feedback, please{' '} 68 | 71 | open an issue! 72 | {' '} 73 | If you're interested in contributing, check out our{' '} 74 | 77 | contribution guide. 78 | 79 | 80 | 81 | 82 |

What is IssueOps?

83 | 84 |

85 | IssueOps is a loose collection of tools, workflows, and concepts that 86 | can be applied to{' '} 87 | 90 | GitHub Issues 91 | {' '} 92 | to drive a nearly limitless number of workflows. Like many of the other 93 | "Ops" tools, (ChatOps, GitOps, and so on), IssueOps leverages 94 | a friendly interface to drive behind-the-scenes automation. In this 95 | case, issues and pull requests (PRs) are the interface, and GitHub 96 | Actions is the automation engine. 97 |

98 | 99 |

100 | IssueOps isn't just a DevOps tool! You can run anything from 101 | complex CI/CD pipelines to a bed and breakfast reservation system. If 102 | you can interact with it via an API, there's a good chance you can 103 | build it with IssueOps! 104 |

105 | 106 | {/* TODO: Animation of IssueOps example */} 107 | 108 |

109 | Why should I use IssueOps? 110 |

111 | 112 | 117 | 118 | {carouselItems.map((item, index) => ( 119 | 120 |
121 | 122 | 123 | 124 |

{item.title}

125 |
126 | {item.description} 127 |
128 |
129 |
130 | ))} 131 |
132 | 133 | 134 |
135 | 136 |

How do I get started?

137 | 138 |

139 | Check out the resources on this site to learn more about IssueOps and 140 | how to build your own workflows. 141 |

142 | 143 |

144 | If you're looking for inspiration and a practical demonstration, 145 | check out{' '} 146 | 149 | Bear Creek Honey Farm 150 | 151 | ! This is a fictional bed and breakfast reservation system drive by 152 | IssueOps workflows. The source code for this example can be found in the{' '} 153 | 156 | issue-ops/bear-creek-honey-farm 157 | {' '} 158 | and{' '} 159 | 162 | issue-ops/demo-reservation-action 163 | {' '} 164 | repositories. 165 |

166 | 167 | 168 | 169 | Have a Cool Example? 170 | 171 | If you have an interesting IssueOps project you'd like featured, 172 | send us a PR! 173 | 174 | 175 |
176 | ) 177 | } 178 | -------------------------------------------------------------------------------- /src/app/reference/branch-deployments/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |

Branch Deployments

9 | 10 |

11 | One interesting topic to include when talking about IssueOps is branch 12 | deployments. At a high-level, branch deployments let you run and control 13 | deployments from your PRs. 14 |

15 | 16 |

17 | If you don't already know what they are, the easiest way to explain 18 | them is to compare them to the traditional merge deploy model. In the 19 | merge deploy model: 20 |

21 | 22 |
    23 |
  1. A developer creates a feature branch and commits changes.
  2. 24 |
  3. The developer opens a PR to get feedback from others.
  4. 25 |
  5. 26 | Once approved, the PR is merged and a deployment starts from the 27 | main branch. 28 |
  6. 29 |
30 | 31 |

32 | This works fine, but if there are bugs in the PR, you have to either 33 | merge in fixes or revert the commits and redeploy. In the branch deploy 34 | model, changes are deployed from the feature branch and validated before 35 | being merged into the main branch. This ensures that 36 | whatever is in main can be deployed at any time. If there 37 | is a problem with the deployment from the feature branch, you can simply 38 | redeploy main as-is. 39 |

40 | 41 |

42 | For a detailed description of the branch-deploy model, see{' '} 43 | 46 | 47 | github/branch-deploy 48 | 49 | 50 | . 51 |

52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/app/reference/examples/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Paper from '@mui/material/Paper' 4 | import Table from '@mui/material/Table' 5 | import TableBody from '@mui/material/TableBody' 6 | import TableCell from '@mui/material/TableCell' 7 | import TableContainer from '@mui/material/TableContainer' 8 | import TableHead from '@mui/material/TableHead' 9 | import TableRow from '@mui/material/TableRow' 10 | import Link from 'next/link' 11 | 12 | export default function Home() { 13 | return ( 14 |
15 |

Examples

16 | 17 |

18 | Here you can find examples of IssueOps workflows and repos from the 19 | developer community. If you have an example you'd like to share, 20 | please open a PR to add it to this page! 21 |

22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | Event 30 | Example 31 | 32 | 33 | 34 | 35 | 36 | 39 | Self-Service IssueOps Template 40 | 41 | 42 | 43 | A self-service approach for managing GitHub components/settings 44 | across multiple instances of GitHub and GitHub Enterprise. 45 | 46 | 47 | 48 | 49 | 52 | Create Blog Posts from Issues 53 | 54 | 55 | 56 | Build and deploy blog posts from issue forms. 57 | 58 | 59 | 60 |
61 |
62 |
63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/app/reference/issueops-actions/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | Paper, 5 | Table, 6 | TableBody, 7 | TableCell, 8 | TableContainer, 9 | TableHead, 10 | TableRow 11 | } from '@mui/material' 12 | import Link from 'next/link' 13 | 14 | export default function Home() { 15 | return ( 16 |
17 |

IssueOps Actions

18 | 19 |

20 | This page contains a list of useful actions for IssueOps workflows. If 21 | you know of any, feel free to submit a PR to add it to the list! 22 |

23 | 24 | 25 | 26 | 27 | 28 | Action 29 | Description 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | actions/add-to-project 40 | 41 | 42 | 43 | Add issues to project boards 44 | 45 | 46 | 47 | 50 | 51 | actions/create-github-app-token 52 | 53 | 54 | 55 | 56 | Create installation access tokens for GitHub Apps 57 | 58 | 59 | 60 | 61 | 64 | 65 | issue-ops/labeler 66 | 67 | 68 | 69 | Bulk add/remove labels 70 | 71 | 72 | 73 | 76 | 77 | issue-ops/parser 78 | 79 | 80 | 81 | Parse an issue into JSON 82 | 83 | 84 | 85 | 88 | 89 | issue-ops/releaser 90 | 91 | 92 | 93 | Automatically create releases 94 | 95 | 96 | 97 | 100 | 101 | issue-ops/semver 102 | 103 | 104 | 105 | Automatically handle version tags 106 | 107 | 108 | 109 | 112 | 113 | issue-ops/validator 114 | 115 | 116 | 117 | Validate issues against custom rules 118 | 119 | 120 | 121 | 124 | 125 | github/command 126 | 127 | 128 | 129 | IssueOps commands for GitHub Actions 130 | 131 | 132 |
133 |
134 |
135 | ) 136 | } 137 | -------------------------------------------------------------------------------- /src/app/setup/github-app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' 4 | import { 5 | Paper, 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableContainer, 10 | TableHead, 11 | TableRow 12 | } from '@mui/material' 13 | import { Ban, CircleAlert, InfoIcon, Lock, Shield } from 'lucide-react' 14 | import Link from 'next/link' 15 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 16 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' 17 | import dedent from 'ts-dedent' 18 | 19 | export default function Home() { 20 | return ( 21 |
22 |

GitHub App

23 | 24 |

25 | If your IssueOps workflow requires access to anything outside of the 26 | repository it is running in, you will need to provide it with a token. 27 | This token is used to authenticate with the GitHub API and should be 28 | scoped to the minimum permissions needed to do the job. Tokens can be 29 | provided two ways: 30 |

31 | 32 | 48 | 49 |

50 | Since PATs are scoped to a single user, they are not recommended for use 51 | in IssueOps workflows. GitHub Apps are a better choice because they can 52 | be scoped to a repository or organization to provide access to the APIs 53 | you need. 54 |

55 | 56 | 57 | 58 | Enterprise Tokens 59 | 60 | GitHub Apps cannot currently be created at the enterprise level for 61 | access to administrative APIs. If you need access to these APIs, you 62 | will need to use a PAT. In these cases, creating a "machine 63 | user" account is recommended over a personal account. 64 | 65 | 66 | 67 |

Ownership

68 | 69 |

70 | When creating a GitHub App, you have the option to specify your personal 71 | account or an organization as the owner. Choosing an organization as the 72 | owner allows you to grant access to multiple repositories in the 73 | organization and simplifies permissions management. 74 |

75 | 76 |

Setup

77 | 78 |

Create a GitHub App

79 | 80 |

81 | For instructions on how to create a GitHub App, see{' '} 82 | 85 | Creating GitHub Apps 86 | 87 | . 88 |

89 | 90 |

91 | The following settings are a good starting point for IssueOps workflows: 92 |

93 | 94 | 95 | 96 | 97 | 98 | Setting 99 | Value 100 | 101 | 102 | 103 | 104 | Name 105 | 106 | A clear name that describes its purpose and permissions 107 | 108 | 109 | 110 | Description 111 | 112 | A description of what the app does and what it can access 113 | 114 | 115 | 116 | Homepage URL 117 | 118 | The URL to the repository with your IssueOps code 119 | 120 | 121 | 122 | Webhook 123 | Disable webhooks 124 | 125 | 126 | Permissions 127 | 128 | Select the minimum permissions needed for your workflow 129 | 130 | 131 | 132 |
133 |
134 | 135 |

Create a private key

136 | 137 |

138 | For instructions on how to create a private key, see{' '} 139 | 142 | Managing private keys for GitHub Apps 143 | 144 | . 145 |

146 | 147 | 148 | 149 | Key Storage 150 | 151 | Make sure to save the private key in a secure location! 152 | 153 | 154 | 155 |

Create GitHub Actions secrets

156 | 157 |

158 | After creating your GitHub App, you will need to create secrets that 159 | your IssueOps workflows can use to authenticate with the GitHub API. You 160 | can create these at the organization, repository, or environment level 161 | depending on your needs. 162 |

163 | 164 |

165 | You will need to create the following secrets. Make sure to note the 166 | names you give them as you will need to reference them in your 167 | workflows. 168 |

169 | 170 | 171 | 172 | 173 | 174 | Name 175 | Description 176 | 177 | 178 | 179 | 180 | App ID 181 | The ID of your GitHub App 182 | 183 | 184 | Private Key 185 | The private key you created 186 | 187 | 188 |
189 |
190 | 191 | 192 | 193 | App ID 194 | 195 | The GitHub App ID is not a sensitive value and can be stored as a 196 | variable instead of a secret. It can be found on the settings page for 197 | your GitHub App. 198 | 199 | 200 | 201 |

For instructions on how to create secrets, see the following links:

202 | 203 | 226 | 227 |

Usage

228 | 229 |

Update the workflow permissions

230 | 231 |

232 | In any workflow that needs to authenticate as a GitHub App, the 233 | following permissions must be specified at the workflow or job 234 | level. 235 |

236 | 237 |
238 | 239 | {dedent` 240 | permissions: 241 | contents: read 242 | id-token: write 243 | `} 244 | 245 |
246 | 247 |

248 | Generate the installation access token 249 |

250 | 251 |

252 | There are various examples and open source actions available to create 253 | installation access tokens for GitHub Actions workflows. In this 254 | documentation, we will use the{' '} 255 | 258 | 259 | actions/create-github-app-token 260 | 261 | {' '} 262 | action. 263 |

264 | 265 |

266 | Within any workflow job that needs to authenticate as your GitHub App, 267 | you will need to include the following step. 268 |

269 | 270 |
271 | 272 | {dedent` 273 | steps: 274 | - uses: actions/create-github-app-token@vX.X.X 275 | id: token 276 | with: 277 | app_id: \${{ secrets.MY_GITHUB_APP_ID }} 278 | private_key: \${{ secrets.MY_GITHUB_APP_PEM }} 279 | owner: \${{ github.repository_owner }} 280 | `} 281 | 282 |
283 | 284 |

Make sure to update the following:

285 | 286 | 295 | 296 | 297 | 298 | Owner 299 | 300 | In the previous example, the owner property is set to the 301 | owner of the repository where this workflow is defined. If your GitHub 302 | App is installed under another owner, you will need to specify that 303 | instead. 304 | 305 | 306 | 307 |

Use the token in your workflow

308 | 309 |

310 | Now that the token is being generated, you can reference it in your 311 | workflows as an output from the token generation step! This can be 312 | referenced as {`\${{ steps..outputs.token }}`}{' '} 313 | (e.g. {`\${{ steps.token.outputs.token }}`}). 314 |

315 | 316 |
317 | 318 | {dedent` 319 | steps: 320 | - uses: actions/github-script@vX.X.X 321 | id: create-org-project 322 | with: 323 | github-token: \${{ steps.token.outputs.token }} 324 | script: | 325 | await github.rest.projects.createForOrg({ 326 | org: 'octo-org', 327 | name: 'My awesome project' 328 | }) 329 | `} 330 | 331 |
332 | 333 | 334 | 335 | Token Usage 336 | 337 | Make sure to check which steps in your workflow will need to use the 338 | GitHub App token versus the workflow token. For example, if you add 339 | the issues: write permission to your workflow, you do not 340 | need to use the GitHub App token to update issues in the same{' '} 341 | repository as your workflows. However, you will need to use the GitHub 342 | App token to update issues in other repositories! 343 | 344 | 345 | 346 |

Example

347 | 348 |

349 | The following can be used as a starting point for your own workflows. 350 | Make sure to update secret names and action versions. 351 |

352 | 353 |
354 | 355 | {dedent` 356 | name: Example Workflow 357 | 358 | # This workflow runs any time an issue is opened or edited. 359 | on: 360 | issues: 361 | types: 362 | - opened 363 | - edited 364 | 365 | jobs: 366 | example-job: 367 | name: Example Job 368 | runs-on: ubuntu-latest 369 | 370 | permissions: 371 | contents: read 372 | id-token: write 373 | 374 | steps: 375 | # Get the GitHub App installation access token. 376 | - uses: actions/create-github-app-token@vX.X.X 377 | id: token 378 | with: 379 | app_id: \${{ secrets.MY_GITHUB_APP_ID }} 380 | private_key: \${{ secrets.MY_GITHUB_APP_PEM }} 381 | owner: \${{ github.repository_owner }} 382 | 383 | - run: echo "Add your custom steps here!" 384 | `} 385 | 386 |
387 |
388 | ) 389 | } 390 | -------------------------------------------------------------------------------- /src/app/setup/issue-form/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' 4 | import { Info } from 'lucide-react' 5 | import Link from 'next/link' 6 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 7 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' 8 | import dedent from 'ts-dedent' 9 | 10 | export default function Home() { 11 | return ( 12 |
13 |

Issue Form

14 | 15 |

16 | The first interaction point users will have with your IssueOps workflow 17 | is the issue itself. This is where they will provide the information 18 | needed to kick off the workflow. Because of this, it is important to 19 | make sure that the issue form template is set up to capture all the 20 | information you need to get started. 21 |

22 | 23 |

24 | For more information on the supported options, see{' '} 25 | 28 | Syntax for issue forms 29 | 30 | . 31 |

32 | 33 |

Top-level syntax

34 | 35 |

36 | The top-level syntax of the issue form template is used to define the 37 | title and description that users will see when they go to create an 38 | issue in your repository. From a usability perspective, make sure to 39 | include a description and title that clearly explains what the user is 40 | requesting and what will happen once they submit their issue. 41 |

42 | 43 | 44 | 45 | Labels and Projects 46 | 47 | Make sure to label your issues and assign them to projects! 48 | 49 | 50 | 51 |
52 | 53 | {dedent` 54 | name: Create a new repository 55 | description: | 56 | Once opened, this issue will cause a new GitHub repository to be created in 57 | the \`octocat\` organization. You will be granted access as a collaborator so 58 | you can build something awesome! 59 | labels: 60 | - issueops:new-repository 61 | projects: 62 | - octocat/123 63 | body: 64 | # ... 65 | `} 66 | 67 |
68 | 69 |

Body syntax

70 | 71 |

72 | The body property is where you specify the inputs and any 73 | other supporting information you need to collect from the user. Its 74 | important to make sure to collect all the information you need to get 75 | started, but also to make sure that the form is not too long or 76 | complicated. 77 |

78 | 79 | 80 | 81 | Labels and Projects 82 | 83 | If there's a way you can calculate certain information, consider 84 | doing that instead of asking the user to provide additional inputs. 85 | For example, you can get the user's GitHub username from the 86 | issue metadata ({`\${{ github.event.issue.user.login }}`} 87 | ) instead of asking them to provide it. 88 | 89 | 90 | 91 |

92 | As you are drafting your issue form template, think about the kind of 93 | data you are requesting and the best format to use (both for user input 94 | and for automated processing later). 95 |

96 | 97 |

98 | This can be confusing, because once an issue form is submitted, all the 99 | inputs look the same. Suppose you have the following issue form 100 | template: 101 |

102 | 103 |
104 | 105 | {dedent` 106 | name: New Repo Request 107 | description: Submit a request to create a new GitHub repository 108 | title: '[Request] New Repository' 109 | labels: 110 | - issueops:new-repository 111 | 112 | body: 113 | # Markdown type fields are not included in the submitted issue body 114 | - type: markdown 115 | attributes: 116 | value: 117 | Welcome to GitHub! Please fill out the information below to request a 118 | new repository. Once submitted, your request will be reviewed by the 119 | IssueOps team. If approved, the repository will be created and you will 120 | be notified via a comment on this issue. 121 | - type: input 122 | id: name 123 | attributes: 124 | label: Repository Name 125 | description: The name of the repository you would like to create. 126 | placeholder: octorepo 127 | validations: 128 | required: true 129 | - type: dropdown 130 | id: visibility 131 | attributes: 132 | label: Repository Visibility 133 | description: The visibility of the repository. 134 | multiple: false 135 | options: 136 | - private 137 | - public 138 | validations: 139 | required: true 140 | - type: dropdown 141 | id: topics 142 | attributes: 143 | label: Repository Topics 144 | description: The topics to add to the repository. 145 | multiple: true 146 | options: 147 | - octocat 148 | - issueops 149 | - automation 150 | validations: 151 | required: true 152 | - type: checkboxes 153 | id: confirm 154 | attributes: 155 | label: Confirmation 156 | description: Do you confirm this request? 157 | options: 158 | - label: 'Yes' 159 | required: true 160 | - label: 'No' 161 | required: false 162 | `} 163 | 164 |
165 | 166 |

167 | When the user submits the issue form, it will have the following 168 | Markdown format: 169 |

170 | 171 |
172 | 176 | {dedent` 177 | ### Repository Name 178 | 179 | octorepo 180 | 181 | ### Repository Visibility 182 | 183 | public 184 | 185 | ### Repository Topics 186 | 187 | octocat, issueops 188 | 189 | ### Confirmation 190 | 191 | - [x] Yes 192 | - [ ] No 193 | `} 194 | 195 |
196 | 197 |

198 | You can see that certain inputs look the same, but are actually 199 | different types and, depending on their values, may be processed 200 | differently. 201 |

202 |
203 | ) 204 | } 205 | -------------------------------------------------------------------------------- /src/app/setup/issue-workflow/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 4 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' 5 | import dedent from 'ts-dedent' 6 | 7 | export default function Home() { 8 | return ( 9 |
10 |

Issue Workflow

11 | 12 |

13 | Once a user submits your issue form, its time for GitHub Actions to run 14 | the show! The issue workflow is responsible for performing any initial 15 | processing of the issue such as adding labels, validating the contents, 16 | adding comments, etc. The following sections will walk through the core 17 | structure of an issue workflow. 18 |

19 | 20 |

Event triggers

21 | 22 |

23 | Most of the time, this workflow will only be run when an issue is opened 24 | or edited, however there are some cases where you may want to run this 25 | workflow when an issue is reopened. 26 |

27 | 28 |
29 | 30 | {dedent` 31 | on: 32 | issues: 33 | types: 34 | - opened 35 | - edited 36 | - reopened 37 | `} 38 | 39 |
40 | 41 |

Jobs

42 | 43 |

44 | Different request types may have different inputs. For example, a new 45 | repository request may have different inputs than a repository transfer 46 | request. If you decide to create multiple jobs to parse different types 47 | of requests in the same workflow, you can use labels to control which 48 | jobs run for which types of requests. 49 |

50 | 51 |

52 | You should also consider how you plan to handle processing of multiple 53 | requests in the same workflow. 54 |

55 | 56 |
57 | 58 | {dedent` 59 | jobs: 60 | new-repository-request: 61 | name: New Repository Request 62 | runs-on: ubuntu-latest 63 | 64 | # Only run for issues with the \`issueops:new-repository\` label. 65 | if: contains(github.event.issue.labels.*.name, 'issueops:new-repository') 66 | 67 | team-membership-request: 68 | name: Team Membership Request 69 | runs-on: ubuntu-latest 70 | 71 | # Only run for issues with the \`issueops:team-add\` label. 72 | if: contains(github.event.issue.labels.*.name, 'issueops:team-add') 73 | `} 74 | 75 |
76 | 77 |

78 | Depending on the complexity of your workflow, you may want to isolate 79 | each type of request to separate jobs entirely, or you may want to have 80 | jobs that handle common tasks across multiple request types. For 81 | example, if you have IssueOps workflows for adding and removing users 82 | from a team, there's a good chance they both have the same input 83 | data and perform the same validation steps. In this case, you may want 84 | to create a job that handles the common tasks, and then have separate 85 | jobs for the unique tasks. 86 |

87 | 88 |
89 | 90 | {dedent` 91 | jobs: 92 | team-request: 93 | name: Team Management Request 94 | runs-on: ubuntu-latest 95 | 96 | # Run this job for both request types 97 | if: | 98 | contains(github.event.issue.labels.*.name, 'issueops:team-add') || 99 | contains(github.event.issue.labels.*.name, 'issueops:team-remove') 100 | 101 | outputs: 102 | request: \${{ steps.parse.outputs.json }} 103 | 104 | steps: 105 | - name: Parse Issue 106 | id: parse 107 | uses: issue-ops/parser@vX.X.X 108 | with: 109 | body: \${{ github.event.issue.body }} 110 | issue-form-template: team-add-remove-request.yml 111 | 112 | - name: Validate Issue 113 | id: validate 114 | uses: issue-ops/validator@vX.X.X 115 | with: 116 | issue-form-template: team-add-remove-request.yml 117 | parsed-issue-body: \${{ steps.parse.outputs.json }} 118 | 119 | team-add: 120 | name: Team Add Request 121 | runs-on: ubuntu-latest 122 | 123 | # Only run after the \`team-request\` job has completed 124 | needs: team-request 125 | 126 | # Only run for issues with the \`issueops:team-add\` label. 127 | if: contains(github.event.issue.labels.*.name, 'issueops:team-add') 128 | 129 | steps: 130 | - name: Add User to Team 131 | id: add 132 | uses: actions/github-script@vX.X.X 133 | with: 134 | github-token: \${{ secrets.MY_TOKEN }} 135 | script: | 136 | const request = JSON.parse('\${{ needs.team-request.outputs.request }}') 137 | 138 | await github.rest.teams.addOrUpdateMembershipForUserInOrg({ 139 | org: request.org, 140 | team_slug: request.team, 141 | username: request.user 142 | }) 143 | 144 | team-remove: 145 | name: Team Remove Request 146 | runs-on: ubuntu-latest 147 | 148 | # Only run after the \`team-request\` job has completed 149 | needs: team-request 150 | 151 | # Only run for issues with the \`issueops:team-remove\` label. 152 | if: contains(github.event.issue.labels.*.name, 'issueops:team-remove') 153 | 154 | steps: 155 | - name: Remove User from Team 156 | id: remove 157 | uses: actions/github-script@vX.X.X 158 | with: 159 | github-token: \${{ secrets.MY_TOKEN }} 160 | script: | 161 | const request = JSON.parse('\${{ needs.team-request.outputs.request }}') 162 | 163 | await github.rest.teams.removeMembershipForUserInOrg({ 164 | org: request.org, 165 | team_slug: request.team, 166 | username: request.user 167 | }) 168 | `} 169 | 170 |
171 |
172 | ) 173 | } 174 | -------------------------------------------------------------------------------- /src/app/setup/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |

Setting up IssueOps

9 | 10 |

11 | This section will walk through the end-to-end setup process for 12 | configuring an IssueOps workflow. This includes setup and configuration 13 | of the following: 14 |

15 | 16 | 53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/app/setup/repository/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Card, CardDescription, CardHeader } from '@/components/ui/card' 4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' 5 | import { 6 | Paper, 7 | Table, 8 | TableBody, 9 | TableCell, 10 | TableContainer, 11 | TableHead, 12 | TableRow 13 | } from '@mui/material' 14 | import Link from 'next/link' 15 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 16 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' 17 | import dedent from 'ts-dedent' 18 | 19 | export default function Home() { 20 | return ( 21 |
22 |

Repository

23 | 24 |

25 | This page outlines recommended configuration settings for IssueOps 26 | repositories. For instructions on how to create a repository, see the{' '} 27 | 30 | GitHub documentation 31 | 32 | . 33 |

34 | 35 |

Visibility

36 | 37 |

38 | IssueOps works best when your repository is accessible to users who need 39 | to submit requests. Depending on if the repository is owned by an 40 | organization or a user account, you can set the visibility to one of the 41 | following: 42 |

43 | 44 | 45 | 46 | 47 | 48 | Owner 49 | Visibility 50 | 51 | 52 | 53 | 54 | Organization 55 | 56 | public or{' '} 57 | internal 58 | 59 | 60 | 61 | User 62 | 63 | public 64 | 65 | 66 | 67 |
68 |
69 | 70 |

71 | Alternatively, if you only want to allow specific users to submit 72 | requests, you can set the visibility to private and add 73 | those users as{' '} 74 | 77 | collaborators 78 | 79 | . 80 |

81 | 82 |

Permissions

83 | 84 |

85 | Users only need read access to open issues! Unless there is 86 | a specific reason to do otherwise, you should only ever need to grant{' '} 87 | read access. 88 |

89 | 90 |

91 | The primary reason to grant write access is if your 92 | IssueOps flow uses pull requests instead of issues, but only if you want 93 | users to create pull requests from branches in the same{' '} 94 | repository. As an alternative, you can allow forking of your repository 95 | and users can create pull requests from their forked repository instead. 96 |

97 | 98 |

Branch protection

99 | 100 |

101 | 104 | Branch protection rules 105 | {' '} 106 | are a good idea regardless of what your repository is being used for! 107 | You should always protect your default branch (usually main 108 | ) and any other branches that you want to prevent accidental changes to. 109 |

110 | 111 |

112 | For an IssueOps repository, you should create a branch protection rule 113 | for main that enables the following: 114 |

115 | 116 | 128 | 129 |

GitHub Actions

130 | 131 |

Fork pull request workflows

132 | 133 |

134 | If your IssueOps workflow uses pull requests instead of issues, you must 135 | be careful about the configuration of GitHub Actions and what 136 | permissions are allowed for fork pull requests. The following settings 137 | can be enabled for fork pull requests, with a description of the risks 138 | involved. 139 |

140 | 141 | 142 | 143 | 144 | 145 | Setting 146 | Risk 147 | 148 | 149 | 150 | 151 | Run workflows from fork pull requests 152 | 153 | Forks will have read permissions to your repository 154 | 155 | 156 | 157 | 158 | Send write tokens to workflows from fork pull requests 159 | 160 | 161 | Forks will have write permissions to your repository 162 | 163 | 164 | 165 | 166 | Send secrets and variables to workflows from fork pull requests 167 | 168 | 169 | Forks will have access to your secrets and variables 170 | 171 | 172 | 173 | 174 | Require approval for fork pull request workflows 175 | 176 | 177 | Forks will not be able to run workflows until they are approved 178 | 179 | 180 | 181 |
182 |
183 | 184 |

185 | As you can guess, the safest option is to not allow fork pull requests 186 | to run workflows at all. However, this may not be practical for your 187 | workflow. Here are some recommended settings: 188 |

189 | 190 | 191 | 192 | Do 193 | Don't 194 | 195 | 196 | 197 | 198 | 199 | Document required permissions for contributors to run the 200 | workflows themselves 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | Send write tokens to fork pull requests 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | Do 219 | Don't 220 | 221 | 222 | 223 | 224 | 225 | Document the required secrets and variables and how to generate 226 | them 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | Send secrets and variables to workflows from fork pull requests 236 | 237 | 238 | 239 | 240 | 241 | 242 |

243 | One alternative to consider is to "wrap" the creation of the 244 | PR into part of your IssueOps flow. If the content of the PR will follow 245 | a known format, you can use a GitHub Action to create the PR on behalf 246 | of the user. This will allow you to remove the need to allow any GitHub 247 | Actions access to fork pull requests. 248 |

249 | 250 |

Workflow permissions

251 | 252 |

253 | In the repository settings, it is best to keep the base workflow 254 | permissions limited to{' '} 255 | Read repository contents and packages permissions. Within each 256 | IssueOps workflow, you can increase the permissions as needed for 257 | specific jobs. 258 |

259 | 260 |

Environments

261 | 262 |

263 | If your IssueOps workflow involves deployments or interaction with 264 | environments, you should consider adding enviroment-specific rules to 265 | restrict deployments to only the main branch. A common 266 | exception to this rule is if you are running IssueOps workflows from 267 | pull requests, as these will be run from branches other than{' '} 268 | main. 269 |

270 | 271 |

272 | This is also a good opportunity to further restrict access to secrets 273 | and variables by defining them at the environment level! 274 |

275 | 276 |

Other considerations

277 | 278 |

279 | A few common questions and answers about repository setup. Most of the 280 | time, the answer is "it depends!", but these are some things 281 | to consider. 282 |

283 | 284 |

285 | Multiple IssueOps workflows in one repository 286 |

287 | 288 |

289 | There are some tradeoffs to consider when using one or multiple 290 | repositories to host different IssueOps workflows. For example, suppose 291 | you have the following workflows: 292 |

293 | 294 | 298 | 299 |

300 | If you use a single repository, one challenge you may run into is 301 | ensuring that jobs for the team membership requests don't affect 302 | new repository requests. This is where labels are particularly helpful! 303 | You can use labels to scope jobs to specific requests. 304 |

305 | 306 |
307 | 308 | {dedent` 309 | name: Issue Opened/Edited 310 | 311 | on: 312 | issues: 313 | types: 314 | - opened 315 | - edited 316 | 317 | jobs: 318 | new-repository-request: 319 | name: New Repository Request 320 | runs-on: ubuntu-latest 321 | 322 | # Only run for issues with the \`issueops:new-repository\` label. 323 | if: contains(github.event.issue.labels.*.name, 'issueops:new-repository') 324 | 325 | team-membership-request: 326 | name: Team Membership Request 327 | runs-on: ubuntu-latest 328 | 329 | # Only run for issues with the \`issueops:team-add\` label. 330 | if: contains(github.event.issue.labels.*.name, 'issueops:team-add') 331 | `} 332 | 333 |
334 |
335 | ) 336 | } 337 | -------------------------------------------------------------------------------- /src/app/states-and-transitions/approve/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 5 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' 6 | import dedent from 'ts-dedent' 7 | 8 | export default function Home() { 9 | return ( 10 |
11 |

Approve

12 | 13 |

14 | In the Approved state, we know that the issue has been 15 | approved and we can begin processing it. This is one of the first states 16 | in the workflow where we can perform an unguarded transition. 17 |

18 | 19 |

20 | In our repository workflow, a request is transitioned to the{' '} 21 | Approved state when an authorized user comments on the 22 | request with .approve. However, immediately after reaching 23 | this state, we know we can create the repository and close the issue 24 | (moving it to the Closed state). This is called an{' '} 25 | unguarded transition because there is no condition that must be 26 | met before the transition occurs. 27 |

28 | 29 |

30 | The actual implementation of this transition is up to you! There are a 31 | few recommendations to keep in mind: 32 |

33 | 34 | 62 | 63 |

New repository request

64 | 65 |

66 | When a new repository request is approved, we need to do the following: 67 |

68 | 69 |
    70 |
  1. Create the repository
  2. 71 |
  3. Comment on the issue
  4. 72 |
  5. Close the issue
  6. 73 |
74 | 75 |
76 | 77 | {dedent` 78 | # This job is responsible for handling approved requests. 79 | approve: 80 | name: Approve Request 81 | runs-on: ubuntu-latest 82 | 83 | # Only run after validation has completed. 84 | needs: validate 85 | 86 | steps: 87 | - name: Approve Command 88 | id: approve 89 | uses: github/command@vX.X.X 90 | with: 91 | allowed_contexts: issue 92 | allowlist: octo-org/approvers 93 | allowlist_pat: \${{ secrets.MY_TOKEN }} 94 | command: .approve 95 | 96 | # Create the repository. 97 | - if: \${{ steps.approve.outputs.continue == 'true' }} 98 | name: Create Repository 99 | id: create 100 | uses: actions/github-script@vX.X.X 101 | with: 102 | github-token: \${{ secrets.MY_TOKEN }} 103 | script: | 104 | const request = JSON.parse('\${{ needs.validate.outputs.request }}') 105 | await github.rest.repos.createInOrg({ 106 | org: '\${{ github.repository_owner }}', 107 | name: request.name, 108 | }) 109 | 110 | # Comment on the issue to let the user know their request was denied. 111 | - if: \${{ steps.approve.outputs.continue == 'true' }} 112 | name: Post Comment 113 | id: comment 114 | uses: peter-evans/create-or-update-comment@vX.X.X 115 | with: 116 | issue-number: \${{ github.event.issue.number }} 117 | body: 118 | ':tada: This request has been approved! Your repository has been 119 | created.' 120 | 121 | # Close the issue. 122 | - if: \${{ steps.approve.outputs.continue == 'true' }} 123 | name: Close Issue 124 | id: close 125 | run: gh issue close \${{ github.event.issue.number }} --reason completed 126 | `} 127 | 128 |
129 | 130 |

Next steps

131 | 132 |

Your IssueOps workflow is officially complete!

133 |
134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /src/app/states-and-transitions/deny/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 5 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' 6 | import dedent from 'ts-dedent' 7 | 8 | export default function Home() { 9 | return ( 10 |
11 |

Deny

12 | 13 |

14 | In the Denied state, we know that the issue has been denied 15 | and there is no further action to take. This is one of the first states 16 | in the workflow where we can perform an unguarded transition. 17 |

18 | 19 |

20 | In our repository workflow, a request is transitioned to the{' '} 21 | Denied 22 | state when an authorized user comments on the request with{' '} 23 | .deny. However, immediately after reaching this state, we 24 | want to close the issue (moving it to the Closed state). 25 | This is called an unguarded transition because there is no 26 | condition that must be met before the transition occurs. 27 |

28 | 29 |

30 | The actual implementation of this transition is up to you! There are a 31 | few recommendations to keep in mind: 32 |

33 | 34 | 61 | 62 |

New repository request

63 | 64 |

65 | When a new repository request is denied, we want to close the issue and 66 | leave a comment for the user. We should also add an appropriate label so 67 | we know the request was closed as denied. 68 |

69 | 70 |
71 | 72 | {dedent` 73 | # This job is responsible for handling denied requests. 74 | deny: 75 | name: Deny Request 76 | runs-on: ubuntu-latest 77 | 78 | # Only run after validation has completed. 79 | needs: validate 80 | 81 | steps: 82 | - name: Deny Command 83 | id: deny 84 | uses: github/command@vX.X.X 85 | with: 86 | allowed_contexts: issue 87 | allowlist: octo-org/approvers 88 | allowlist_pat: \${{ secrets.MY_TOKEN }} 89 | command: .deny 90 | 91 | # Comment on the issue to let the user know their request was denied. 92 | - if: \${{ steps.deny.outputs.continue == 'true' }} 93 | name: Post Comment 94 | id: comment 95 | uses: peter-evans/create-or-update-comment@vX.X.X 96 | with: 97 | issue-number: \${{ github.event.issue.number }} 98 | body: 99 | ':no_entry_sign: This request has been denied! This issue will be 100 | closed shortly.' 101 | 102 | # Close the issue. 103 | - if: \${{ steps.deny.outputs.continue == 'true' }} 104 | name: Close Issue 105 | id: close 106 | run: gh issue close \${{ github.event.issue.number }} --reason not_planned 107 | `} 108 | 109 |
110 | 111 |

Next steps

112 | 113 |

Your IssueOps workflow is officially complete!

114 |
115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /src/app/states-and-transitions/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' 4 | import { MermaidDiagram } from '@lightenna/react-mermaid-diagram' 5 | import { 6 | Paper, 7 | Table, 8 | TableBody, 9 | TableCell, 10 | TableContainer, 11 | TableHead, 12 | TableRow 13 | } from '@mui/material' 14 | import Link from 'next/link' 15 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 16 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' 17 | import dedent from 'ts-dedent' 18 | 19 | export default function Home() { 20 | return ( 21 |
22 |

States and Transitions

23 | 24 |

25 | As the{' '} 26 | 27 | introduction 28 | {' '} 29 | mentioned, IssueOps can be thought of as a state diagram where an issue 30 | transitions through different states in response to events and 31 | conditions. This section of the documentation describes some common 32 | states, transitions. and how to implement them in your workflows. 33 |

34 | 35 |

States

36 | 37 |

38 | If an issue was a paper form, a state would be a big rubber stamp that 39 | tells anyone who looks at the form exactly what is going on. 40 |

41 | 42 |

43 | Tracking state can be as simple as checking what labels are applied to 44 | your issues. Whatever approach you want to take, remember that tracking 45 | state is critical to ensure that the right processing occurs at the 46 | right time! 47 |

48 | 49 | 50 | 51 | 52 | 53 | State 54 | Description 55 | 56 | 57 | 58 | 59 | 60 | Opened 61 | 62 | 63 | The initial state for a new issue that has been opened. 64 | Typically the first state in the lifecycle of an issue. 65 | 66 | 67 | 68 | 69 | Parsed 70 | 71 | 72 | The issue body has been read and converted to machine-readable 73 | JSON. Usually the next immediate state after{' '} 74 | Opened. 75 | 76 | 77 | 78 | 79 | Validated 80 | 81 | 82 | The issue body has been deemed valid based on any custom rules. 83 | Usually the next immediate state after{' '} 84 | Parsed. The next 85 | transitions depend on the type of request and any rules that 86 | must be followed. 87 | 88 | 89 | 90 | 91 | Submitted 92 | 93 | 94 | The issue has been submitted for processing. 95 | 96 | 97 | 98 | 99 | Approved 100 | 101 | The issue has been approved for processing. 102 | 103 | 104 | 105 | Denied 106 | 107 | The issue has been denied for processing. 108 | 109 | 110 | 111 | Closed 112 | 113 | The issue has been closed! 114 | 115 | 116 |
117 |
118 | 119 | 120 | 121 | Example State Diagram 122 | 123 | 124 | 125 | {dedent` 126 | stateDiagram-v2 127 | 1 : Opened 128 | 2 : Parsed 129 | 3 : Validated 130 | 4 : Submitted 131 | 5 : Approved 132 | 6 : Denied 133 | 7 : Closed 134 | [*] --> 1 135 | 1 --> 1 : Parse failure 136 | 1 --> 2 : Parse success 137 | 2 --> 2 : Validate failure 138 | 2 --> 3 : Validate success 139 | 3 --> 4 : Submit for processing 140 | 4 --> 5 : Approve request 141 | 4 --> 6 : Deny request 142 | 5 --> 7 : Process request 143 | 6 --> 7 : Notify user 144 | 7 --> [*] 145 | `} 146 | 147 | 148 | 149 | 150 |

Transitions

151 | 152 |

153 | If an issue was a paper form, a transition would be someone taking it 154 | out of their inbox, stamping it APPROVED, and putting 155 | it in their outbox. 156 |

157 | 158 |

159 | Transitions are where actual processing on your issues occurs. A 160 | transition is equivalent to an event that triggers a GitHub Actions 161 | workflow run. That is why, as your IssueOps workflows become larger and 162 | more complex, tracking state is so important. Otherwise , its easy to 163 | end up the wrong workflows running at the wrong time! 164 |

165 | 166 |

Since each transition is triggered by the same type of event:

167 | 168 |
169 | 170 | {dedent` 171 | on: 172 | issue_comment: 173 | types: 174 | - created 175 | `} 176 | 177 |
178 | 179 |

180 | Your workflows must track the following to determine what jobs to run: 181 |

182 | 183 | 189 | 190 |

191 | Each of the following sections describes how to implement the core 192 | transitions in an IssueOps workflow. Throughout each page, you will see 193 | an example implementation of a new repository request workflow. This 194 | workflow is designed to demonstrate how to apply each concept. 195 |

196 | 197 |

198 | A full example can be found in the{' '} 199 | 202 | 203 | issue-ops/bear-creek-honey-farm 204 | 205 | {' '} 206 | and{' '} 207 | 210 | 211 | issue-ops/demo-reservation-action 212 | 213 | {' '} 214 | repositories. 215 |

216 | 217 | 254 | 255 |

FAQ

256 | 257 |

258 | Do my IssueOps need all these states? 259 |

260 | 261 |

262 | Nope! You can use as many or as few states as you need. For example, if 263 | you don't need an authorized user to approve requests, you can omit 264 | the Approved state. 265 |

266 | 267 |

268 | Can my IssueOps use each state more than once? 269 |

270 | 271 |

272 | Of course! In state diagrams, its common for each state to have multiple 273 | transitions. States can even transition back into themselves! 274 |

275 | 276 |

277 | When an issue is first opened it will be in the Opened{' '} 278 | state. Typically, the first step after an issue is opened is to parse 279 | the body and prepare for further processing. If this is successful, the 280 | issue would transition into the Parsed state. If there is a 281 | problem during parsing, the workflow can add a comment with a 282 | descriptive error and return the issue to the Opened state. 283 | When the user edits the issue to fix the error, parsing would run again. 284 |

285 | 286 | 287 | 288 | Example State Diagram 289 | 290 | 291 | 292 | {dedent` 293 | stateDiagram-v2 294 | 1 : Opened 295 | 2 : Parsed 296 | [*] --> 1 297 | 1 --> 2 : Parse success 298 | 1 --> 1 : Parse failure 299 | 2 --> [*] 300 | `} 301 | 302 | 303 | 304 |
305 | ) 306 | } 307 | -------------------------------------------------------------------------------- /src/app/states-and-transitions/submit/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' 4 | import { KeyRound } from 'lucide-react' 5 | import Link from 'next/link' 6 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 7 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' 8 | import dedent from 'ts-dedent' 9 | 10 | export default function Home() { 11 | return ( 12 |
13 |

Submit

14 | 15 |

16 | Once your issue has been parsed and validated, it's ready for 17 | processing! At this point, _processing_ can mean a lot of things and is 18 | entirely dependent on your use case. For example, if you're using 19 | IssueOps to access administrative functions, you may require a human to 20 | review and approve the issue. Or, if you're using IssueOps to track 21 | PTO requests, you may not need any additional approvals and can simply 22 | mark the issue as processed. 23 |

24 | 25 |

26 | This page walks through the process of submitting a request after it has 27 | been validated. In particular, it covers requesting approval from 28 | authorized users or teams. 29 |

30 | 31 |

32 | 33 | Wouldn't opening the issue count as the act of submitting it? 34 | 35 |

36 | 37 |

38 | Absolutely! However, the act of opening an issue may not be the best 39 | indicator that an issue is in the Submitted state in your 40 | workflow. What if you need to do additional processing on the validated 41 | request which requires confirmation from the user? 42 |

43 | 44 |

45 | Using the new repository request as an example, your organization may 46 | want to enforce certain naming conventions for repositories, such as 47 | prefixing the name with the user's department. In this case, when a 48 | user opens a request and asks for a repository named{' '} 49 | pto-requests, you could have them confirm that the 50 | generated name of hr-pto-requests is acceptable before 51 | submitting the request for further processing. 52 |

53 | 54 |

Command actions

55 | 56 |

57 | This is where the{' '} 58 | 61 | github/command 62 | {' '} 63 | action comes into play. This action allows you to specify the who 64 | ,what, when, and where of activities that can be 65 | performed on an issue. For example, if you request approval for a new 66 | repository, the github/command action ensures that any user 67 | cannot approve the request. Instead, only users or teams you specify 68 | can. 69 |

70 | 71 | 72 | 73 | Token Permissions 74 | 75 | As with other actions that call GitHub APIs, if you want to include 76 | GitHub teams in the allowlist feature, you must provide a 77 | valid token in the allowlist_pat input. This can be a 78 | token generated from a GitHub App! 79 | 80 | 81 | 82 |
83 | 84 | {dedent` 85 | steps: 86 | - name: Approve Command 87 | id: approve 88 | uses: github/command@vX.X.X 89 | with: 90 | allowed_contexts: issue 91 | allowlist: octo-org/approvers 92 | allowlist_pat: \${{ secrets.MY_TOKEN }} 93 | command: .approve 94 | `} 95 | 96 |
97 | 98 |

99 | This step acts as the gate for any further processing of the issue. The{' '} 100 | continue output can be used to conditionally invoke further 101 | steps. For example, if the continue output is{' '} 102 | 'true', the user who commented on the issue with{' '} 103 | .approve was indeed authorized to approve the request. 104 |

105 | 106 |
107 | 108 | {dedent` 109 | steps: 110 | - name: Approve Command 111 | id: approve 112 | uses: github/command@vX.X.X 113 | with: 114 | allowed_contexts: issue 115 | allowlist: octo-org/approvers 116 | allowlist_pat: \${{ secrets.MY_TOKEN }} 117 | command: .approve 118 | 119 | ############################################## 120 | # This is a great time to re-run validation! # 121 | ############################################## 122 | 123 | - if: \${{ steps.approve.outputs.continue == 'true' }} 124 | run: echo "This request is approved!" 125 | `} 126 | 127 |
128 | 129 |

130 | With any approval workflow, you should also consider what happens when a 131 | request is explicitly denied This is easy to implement as a separate{' '} 132 | github/command step that looks for the .deny{' '} 133 | command. As with the approval command, if the user who commented on the 134 | issue is authorized to deny requests, the continue output 135 | would be 'true'. 136 |

137 | 138 |
139 | 140 | {dedent` 141 | steps: 142 | - name: Approve Command 143 | id: approve 144 | uses: github/command@vX.X.X 145 | with: 146 | allowed_contexts: issue 147 | allowlist: octo-org/approvers 148 | allowlist_pat: \${{ secrets.MY_TOKEN }} 149 | command: .approve 150 | 151 | - name: Deny Command 152 | id: deny 153 | uses: github/command@vX.X.X 154 | with: 155 | allowed_contexts: issue 156 | allowlist: octo-org/approvers 157 | allowlist_pat: \${{ secrets.MY_TOKEN }} 158 | command: .deny 159 | 160 | - if: \${{ steps.approve.outputs.continue == 'true' }} 161 | run: echo "This request is approved :)" 162 | 163 | - if: \${{ steps.deny.outputs.continue == 'true' }} 164 | run: echo "This request is denied :(" 165 | `} 166 | 167 |
168 | 169 |

New repository request

170 | 171 |

172 | Up until this point, everything has been handled as part of the issue 173 | creation workflow. Now that the issue has been validated, any further 174 | processing is done via comments, labels, reactions, and so on. 175 |

176 | 177 |

Create the comment workflow file

178 | 179 |

180 | The first step is to create a workflow file that will be triggered when 181 | a user comments on an issue. This workflow file will be responsible for 182 | parsing the comment and determining the following: 183 |

184 | 185 | 192 | 193 |

194 | In this example, we will set up two different jobs that will run when 195 | the request is approved or denied. 196 |

197 | 198 |
199 | 200 | {dedent` 201 | name: Issue Comment 202 | 203 | # This workflow runs any time a comment is added to an issue. The comment body 204 | # is read and used to determine what action to take. 205 | on: 206 | issue_comment: 207 | types: 208 | - created 209 | 210 | jobs: 211 | # This job handles the case where a user comments with \`.approve\`. 212 | approve: 213 | name: Approve Request 214 | runs-on: ubuntu-latest 215 | 216 | steps: 217 | - name: Approve Command 218 | id: approve 219 | uses: github/command@vX.X.X 220 | with: 221 | allowed_contexts: issue 222 | allowlist: octo-org/approvers 223 | allowlist_pat: \${{ secrets.MY_TOKEN }} 224 | command: .approve 225 | 226 | - if: \${{ steps.approve.outputs.continue == 'true' }} 227 | run: echo "This request is approved!" 228 | 229 | # This job handles the case where a user comments with \`.deny\`. 230 | deny: 231 | name: Deny Request 232 | runs-on: ubuntu-latest 233 | 234 | steps: 235 | - name: Deny Command 236 | id: deny 237 | uses: github/command@vX.X.X 238 | with: 239 | allowed_contexts: issue 240 | allowlist: octo-org/approvers 241 | allowlist_pat: \${{ secrets.MY_TOKEN }} 242 | command: .deny 243 | 244 | - if: \${{ steps.deny.outputs.continue == 'true' }} 245 | run: echo "This request is denied!" 246 | `} 247 | 248 |
249 | 250 |

Trigger the workflow

251 | 252 |

253 | In the above workflow, both the approve and{' '} 254 | deny jobs are triggered when a user comments on an issue or 255 | PR. Though the github/command actions will act as one gate, 256 | you may want to add additional conditions to ensure that the workflow is 257 | not run when the issue is in a state that does not require approval. For 258 | example, this workflow doesn't need to run if: 259 |

260 | 261 | 268 | 269 |

270 | Workflow conditions can be used to control when the workflow jobs are 271 | invoked. 272 |

273 | 274 |
275 | 276 | {dedent` 277 | name: Issue Comment 278 | 279 | on: 280 | issue_comment: 281 | types: 282 | - created 283 | 284 | jobs: 285 | approve: 286 | name: Approve Request 287 | runs-on: ubuntu-latest 288 | 289 | # Only run when the following conditions are true: 290 | # - The issue has the \`issueops:new-repository\` label 291 | # - The issue has the \`issueops:validated\` label 292 | # - The issue does not have the \`issueops:approved\` label 293 | # - The issue is open 294 | if: | 295 | contains(github.event.issue.labels.*.name, 'issueops:new-repository') && 296 | contains(github.event.issue.labels.*.name, 'issueops:validated') && 297 | contains(github.event.issue.labels.*.name, 'issueops:approved') == false && 298 | github.event.issue.state == 'open' 299 | 300 | steps: 301 | # ... 302 | 303 | deny: 304 | name: Deny Request 305 | runs-on: ubuntu-latest 306 | 307 | # Only run when the following conditions are true: 308 | # - The issue has the \`issueops:new-repository\` label 309 | # - The issue has the \`issueops:validated\` label 310 | # - The issue does not have the \`issueops:approved\` label 311 | # - The issue is open 312 | if: | 313 | contains(github.event.issue.labels.*.name, 'issueops:new-repository') && 314 | contains(github.event.issue.labels.*.name, 'issueops:validated') && 315 | contains(github.event.issue.labels.*.name, 'issueops:approved') == false && 316 | github.event.issue.state == 'open' 317 | 318 | steps: 319 | # ... 320 | `} 321 | 322 |
323 | 324 |

325 | This seems like duplication of the same checks. Plus, we haven't 326 | followed our own rule: Validate Early. Validate Often. Instead, 327 | lets move this to a separate job that re-runs validation checks. 328 |

329 | 330 |
331 | 332 | {dedent` 333 | name: Issue Comment 334 | 335 | on: 336 | issue_comment: 337 | types: 338 | - created 339 | 340 | jobs: 341 | validate: 342 | name: Validate Request 343 | runs-on: ubuntu-latest 344 | 345 | # Only run when the following conditions are true: 346 | # - The issue has the \`issueops:new-repository\` label 347 | # - The issue has the \`issueops:validated\` label 348 | # - The issue does not have the \`issueops:approved\` label 349 | # - The issue is open 350 | if: | 351 | contains(github.event.issue.labels.*.name, 'issueops:new-repository') && 352 | contains(github.event.issue.labels.*.name, 'issueops:validated') && 353 | contains(github.event.issue.labels.*.name, 'issueops:approved') == false && 354 | github.event.issue.state == 'open' 355 | 356 | permissions: 357 | contents: read 358 | id-token: write 359 | issues: write 360 | 361 | outputs: 362 | request: \${{ steps.parse.outputs.request }} 363 | 364 | steps: 365 | - name: Remove Labels 366 | id: remove-label 367 | uses: issue-ops/labeler@vX.X.X 368 | with: 369 | action: remove 370 | issue_number: \${{ github.event.issue.number }} 371 | labels: | 372 | issueops:validated 373 | issueops:submitted 374 | 375 | - name: Get App Token 376 | id: token 377 | uses: actions/create-github-app-token@vX.X.X 378 | with: 379 | app_id: \${{ secrets.MY_GITHUB_APP_ID }} 380 | private_key: \${{ secrets.MY_GITHUB_APP_PEM }} 381 | owner: \${{ github.repository_owner }} 382 | 383 | - name: Checkout 384 | id: checkout 385 | uses: actions/checkout@vX.X.X 386 | 387 | - name: Setup Node.js 388 | id: setup-node 389 | uses: actions/setup-node@vX.X.X 390 | with: 391 | node-version-file: .node-version 392 | cache: npm 393 | 394 | - name: Install Packages 395 | id: npm 396 | run: npm ci 397 | 398 | - name: Parse Issue 399 | id: parse 400 | uses: issue-ops/parser@vX.X.X 401 | with: 402 | body: \${{ github.event.issue.body }} 403 | issue-form-template: new-repository-request.yml 404 | 405 | - name: Validate Issue 406 | id: validate 407 | uses: issue-ops/validator@vX.X.X 408 | with: 409 | issue-form-template: new-repository-request.yml 410 | github-token: \${{ steps.token.outputs.token }} 411 | parsed-issue-body: \${{ steps.parse.outputs.json }} 412 | 413 | - if: \${{ steps.validate.outputs.result == 'success' }} 414 | name: Add Validated Label 415 | id: add-label 416 | uses: issue-ops/labeler@vX.X.X 417 | with: 418 | action: add 419 | issue_number: \${{ github.event.issue.number }} 420 | labels: | 421 | issueops:validated 422 | 423 | approve: 424 | name: Approve Request 425 | runs-on: ubuntu-latest 426 | 427 | # Only run after validation has completed. 428 | needs: validate 429 | 430 | steps: 431 | # ... 432 | 433 | deny: 434 | name: Deny Request 435 | runs-on: ubuntu-latest 436 | 437 | # Only run after validation has completed. 438 | needs: validate 439 | 440 | steps: 441 | # ... 442 | `} 443 | 444 |
445 | 446 |

447 | With this workflow, we know that the request has been validated before 448 | we handle any approval or denial. This is a good example of{' '} 449 | Validate Early. Validate Often. 450 |

451 | 452 |

Next steps

453 | 454 |

455 | Depending on if the request is approved or denied, you may want to take 456 | further actions. For example, if the request is approved, you could 457 | create the repository, add a comment to the issue, and close it as 458 | completed. On the other hand, if the request is denied, you could close 459 | the issue as not planned. 460 |

461 | 462 |

463 | Continue to the{' '} 464 | 467 | approve 468 | {' '} 469 | or{' '} 470 | 473 | deny 474 | {' '} 475 | sections to learn more. 476 |

477 |
478 | ) 479 | } 480 | -------------------------------------------------------------------------------- /src/components/app-sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { NavMain } from '@/components/nav-main' 4 | import { 5 | Sidebar, 6 | SidebarContent, 7 | SidebarFooter, 8 | SidebarHeader, 9 | SidebarMenu, 10 | SidebarMenuButton, 11 | SidebarMenuItem, 12 | SidebarRail 13 | } from '@/components/ui/sidebar' 14 | import { MarkGithubIcon } from '@primer/octicons-react' 15 | import { 16 | DropdownMenu, 17 | DropdownMenuTrigger 18 | } from '@radix-ui/react-dropdown-menu' 19 | import { 20 | BookOpen, 21 | Bot, 22 | DockIcon, 23 | GalleryVerticalEnd, 24 | type LucideIcon, 25 | Settings2, 26 | SquareTerminal 27 | } from 'lucide-react' 28 | import Link from 'next/link' 29 | import * as React from 'react' 30 | import { NavFooter } from './nav-footer' 31 | 32 | const data = { 33 | teams: [ 34 | { 35 | name: 'Acme Inc', 36 | logo: GalleryVerticalEnd, 37 | plan: 'Enterprise' 38 | } 39 | ], 40 | navMain: [ 41 | { 42 | title: 'Introduction', 43 | url: '#', 44 | icon: SquareTerminal, 45 | isActive: true, 46 | items: [ 47 | { 48 | title: 'About', 49 | url: '/docs/introduction' 50 | }, 51 | { 52 | title: 'Issues and PRs', 53 | url: '/docs/introduction/issues-and-prs' 54 | }, 55 | { 56 | title: 'Workflow Security', 57 | url: '/docs/introduction/workflow-security' 58 | }, 59 | { 60 | title: 'Best Practices', 61 | url: '/docs/introduction/best-practices' 62 | } 63 | ] 64 | }, 65 | { 66 | title: 'Setup', 67 | url: '#', 68 | icon: Bot, 69 | items: [ 70 | { 71 | title: 'About', 72 | url: '/docs/setup' 73 | }, 74 | { 75 | title: 'Repository', 76 | url: '/docs/setup/repository' 77 | }, 78 | { 79 | title: 'GitHub App', 80 | url: '/docs/setup/github-app' 81 | }, 82 | { 83 | title: 'Issue Form', 84 | url: '/docs/setup/issue-form' 85 | }, 86 | { 87 | title: 'Issue Workflow', 88 | url: '/docs/setup/issue-workflow' 89 | }, 90 | { 91 | title: 'Comment Workflow', 92 | url: '/docs/setup/comment-workflow' 93 | } 94 | ] 95 | }, 96 | { 97 | title: 'States and Transitions', 98 | url: '#', 99 | icon: BookOpen, 100 | items: [ 101 | { 102 | title: 'About', 103 | url: '/docs/states-and-transitions' 104 | }, 105 | { 106 | title: 'Parse', 107 | url: '/docs/states-and-transitions/parse' 108 | }, 109 | { 110 | title: 'Validate', 111 | url: '/docs/states-and-transitions/validate' 112 | }, 113 | { 114 | title: 'Submit', 115 | url: '/docs/states-and-transitions/submit' 116 | }, 117 | { 118 | title: 'Approve', 119 | url: '/docs/states-and-transitions/approve' 120 | }, 121 | { 122 | title: 'Deny', 123 | url: '/docs/states-and-transitions/deny' 124 | } 125 | ] 126 | }, 127 | { 128 | title: 'Reference', 129 | url: '#', 130 | icon: Settings2, 131 | items: [ 132 | { 133 | title: 'IssueOps Actions', 134 | url: '/docs/reference/issueops-actions' 135 | }, 136 | { 137 | title: 'Branch Deployments', 138 | url: '/docs/reference/branch-deployments' 139 | }, 140 | { 141 | title: 'Examples', 142 | url: '/docs/reference/examples' 143 | } 144 | ] 145 | } 146 | ] 147 | } 148 | 149 | const footer = [ 150 | { 151 | name: 'issue-ops/docs', 152 | url: 'https://github.com/issue-ops/docs', 153 | icon: MarkGithubIcon as LucideIcon 154 | } 155 | ] 156 | 157 | export function AppSidebar({ ...props }: React.ComponentProps) { 158 | return ( 159 | 160 | 161 | 162 | 163 | 164 | 165 | 169 | 170 |
171 | 172 |
173 |
174 | 175 | IssueOps Docs 176 | 177 |
178 | 179 |
180 |
181 |
182 |
183 |
184 |
185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 |
193 | ) 194 | } 195 | -------------------------------------------------------------------------------- /src/components/nav-footer.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | SidebarGroup, 5 | SidebarGroupLabel, 6 | SidebarMenu, 7 | SidebarMenuButton, 8 | SidebarMenuItem 9 | } from '@/components/ui/sidebar' 10 | import { type LucideIcon } from 'lucide-react' 11 | 12 | export function NavFooter({ 13 | projects 14 | }: { 15 | projects: { 16 | name: string 17 | url: string 18 | icon: LucideIcon 19 | }[] 20 | }) { 21 | return ( 22 | 23 | GitHub 24 | 25 | {projects.map((item) => ( 26 | 27 | 28 | 29 | 30 | {item.name} 31 | 32 | 33 | 34 | ))} 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/nav-main.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ChevronRight, type LucideIcon } from 'lucide-react' 4 | 5 | import { 6 | Collapsible, 7 | CollapsibleContent, 8 | CollapsibleTrigger 9 | } from '@/components/ui/collapsible' 10 | import { 11 | SidebarGroup, 12 | SidebarMenu, 13 | SidebarMenuButton, 14 | SidebarMenuItem, 15 | SidebarMenuSub, 16 | SidebarMenuSubButton, 17 | SidebarMenuSubItem 18 | } from '@/components/ui/sidebar' 19 | 20 | export function NavMain({ 21 | items 22 | }: { 23 | items: { 24 | title: string 25 | url: string 26 | icon?: LucideIcon 27 | isActive?: boolean 28 | items?: { 29 | title: string 30 | url: string 31 | }[] 32 | }[] 33 | }) { 34 | return ( 35 | 36 | 37 | {items.map((item) => ( 38 | 43 | 44 | 45 | 46 | {item.icon && } 47 | {item.title} 48 | 49 | 50 | 51 | 52 | 53 | {item.items?.map((subItem) => ( 54 | 55 | 56 | 57 | {subItem.title} 58 | 59 | 60 | 61 | ))} 62 | 63 | 64 | 65 | 66 | ))} 67 | 68 | 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/components/nav-projects.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | Folder, 5 | Forward, 6 | MoreHorizontal, 7 | Trash2, 8 | type LucideIcon 9 | } from 'lucide-react' 10 | 11 | import { 12 | DropdownMenu, 13 | DropdownMenuContent, 14 | DropdownMenuItem, 15 | DropdownMenuSeparator, 16 | DropdownMenuTrigger 17 | } from '@/components/ui/dropdown-menu' 18 | import { 19 | SidebarGroup, 20 | SidebarGroupLabel, 21 | SidebarMenu, 22 | SidebarMenuAction, 23 | SidebarMenuButton, 24 | SidebarMenuItem, 25 | useSidebar 26 | } from '@/components/ui/sidebar' 27 | 28 | export function NavProjects({ 29 | projects 30 | }: { 31 | projects: { 32 | name: string 33 | url: string 34 | icon: LucideIcon 35 | }[] 36 | }) { 37 | const { isMobile } = useSidebar() 38 | 39 | return ( 40 | 41 | Projects 42 | 43 | {projects.map((item) => ( 44 | 45 | 46 | 47 | 48 | {item.name} 49 | 50 | 51 | 52 | 53 | 54 | 55 | More 56 | 57 | 58 | 62 | 63 | 64 | View Project 65 | 66 | 67 | 68 | Share Project 69 | 70 | 71 | 72 | 73 | Delete Project 74 | 75 | 76 | 77 | 78 | ))} 79 | 80 | 81 | 82 | More 83 | 84 | 85 | 86 | 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /src/components/nav-user.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | BadgeCheck, 5 | Bell, 6 | ChevronsUpDown, 7 | CreditCard, 8 | LogOut, 9 | Sparkles 10 | } from 'lucide-react' 11 | 12 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' 13 | import { 14 | DropdownMenu, 15 | DropdownMenuContent, 16 | DropdownMenuGroup, 17 | DropdownMenuItem, 18 | DropdownMenuLabel, 19 | DropdownMenuSeparator, 20 | DropdownMenuTrigger 21 | } from '@/components/ui/dropdown-menu' 22 | import { 23 | SidebarMenu, 24 | SidebarMenuButton, 25 | SidebarMenuItem, 26 | useSidebar 27 | } from '@/components/ui/sidebar' 28 | 29 | export function NavUser({ 30 | user 31 | }: { 32 | user: { 33 | name: string 34 | email: string 35 | avatar: string 36 | } 37 | }) { 38 | const { isMobile } = useSidebar() 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | CN 51 | 52 |
53 | {user.name} 54 | {user.email} 55 |
56 | 57 |
58 |
59 | 64 | 65 |
66 | 67 | 68 | CN 69 | 70 |
71 | {user.name} 72 | {user.email} 73 |
74 |
75 |
76 | 77 | 78 | 79 | 80 | Upgrade to Pro 81 | 82 | 83 | 84 | 85 | 86 | 87 | Account 88 | 89 | 90 | 91 | Billing 92 | 93 | 94 | 95 | Notifications 96 | 97 | 98 | 99 | 100 | 101 | Log out 102 | 103 |
104 |
105 |
106 |
107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-background text-foreground', 12 | destructive: 13 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive' 14 | } 15 | }, 16 | defaultVariants: { 17 | variant: 'default' 18 | } 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = 'Alert' 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = 'AlertTitle' 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = 'AlertDescription' 58 | 59 | export { Alert, AlertDescription, AlertTitle } 60 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as AvatarPrimitive from '@radix-ui/react-avatar' 4 | import * as React from 'react' 5 | 6 | import { cn } from '@/lib/utils' 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarFallback, AvatarImage } 51 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot' 2 | import { ChevronRight, MoreHorizontal } from 'lucide-react' 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<'nav'> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>