├── .ddev ├── commands │ └── host │ │ └── npm └── config.yaml ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── __tests__ ├── commit-message-checker.test.ts └── input-helper.test.ts ├── action.yml ├── dist └── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── commit-message-checker.ts ├── input-helper.ts └── main.ts └── tsconfig.json /.ddev/commands/host/npm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Description: Run npm inside the web container in the root of the project (Use --cwd for another directory) 4 | ## Usage: npm [flags] [args] 5 | ## Example: "ddev npm install" or "ddev npm add learna" or "ddev npm --cwd web/core add learna" 6 | 7 | ddev exec --raw bash -ic "npm $*" 8 | ddev mutagen sync 2>/dev/null 9 | -------------------------------------------------------------------------------- /.ddev/config.yaml: -------------------------------------------------------------------------------- 1 | name: commit-message-checker 2 | type: php 3 | docroot: "" 4 | php_version: "7.4" 5 | webserver_type: nginx-fpm 6 | router_http_port: "80" 7 | router_https_port: "443" 8 | xdebug_enabled: false 9 | additional_hostnames: [] 10 | additional_fqdns: [] 11 | database: 12 | type: mariadb 13 | version: "10.3" 14 | nfs_mount_enabled: false 15 | mutagen_enabled: false 16 | hooks: 17 | post-start: 18 | - exec: npm install 19 | omit_containers: [db, dba, ddev-ssh-agent] 20 | use_dns_when_possible: true 21 | composer_version: "" 22 | web_environment: [] 23 | nodejs_version: "16" 24 | 25 | # Key features of ddev's config.yaml: 26 | 27 | # name: # Name of the project, automatically provides 28 | # http://projectname.ddev.site and https://projectname.ddev.site 29 | 30 | # type: # drupal6/7/8, backdrop, typo3, wordpress, php 31 | 32 | # docroot: # Relative path to the directory containing index.php. 33 | 34 | # php_version: "7.4" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1" 35 | 36 | # You can explicitly specify the webimage but this 37 | # is not recommended, as the images are often closely tied to ddev's' behavior, 38 | # so this can break upgrades. 39 | 40 | # webimage: # nginx/php docker image. 41 | 42 | # database: 43 | # type: # mysql, mariadb 44 | # version: # database version, like "10.3" or "8.0" 45 | # Note that mariadb_version or mysql_version from v1.18 and earlier 46 | # will automatically be converted to this notation with just a "ddev config --auto" 47 | 48 | # router_http_port: # Port to be used for http (defaults to port 80) 49 | # router_https_port: # Port for https (defaults to 443) 50 | 51 | # xdebug_enabled: false # Set to true to enable xdebug and "ddev start" or "ddev restart" 52 | # Note that for most people the commands 53 | # "ddev xdebug" to enable xdebug and "ddev xdebug off" to disable it work better, 54 | # as leaving xdebug enabled all the time is a big performance hit. 55 | 56 | # xhprof_enabled: false # Set to true to enable xhprof and "ddev start" or "ddev restart" 57 | # Note that for most people the commands 58 | # "ddev xhprof" to enable xhprof and "ddev xhprof off" to disable it work better, 59 | # as leaving xhprof enabled all the time is a big performance hit. 60 | 61 | # webserver_type: nginx-fpm # or apache-fpm 62 | 63 | # timezone: Europe/Berlin 64 | # This is the timezone used in the containers and by PHP; 65 | # it can be set to any valid timezone, 66 | # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 67 | # For example Europe/Dublin or MST7MDT 68 | 69 | # composer_version: "2" 70 | # if composer_version:"2" it will use the most recent composer v2 71 | # It can also be set to "1", to get most recent composer v1 72 | # or "" for the default v2 created at release time. 73 | # It can be set to any existing specific composer version. 74 | # After first project 'ddev start' this will not be updated until it changes 75 | 76 | # nodejs_version: "16" 77 | # change from the default system Node.js version to another supported version, like 12, 14, 17. 78 | # Note that you can use 'ddev nvm' or nvm inside the web container to provide nearly any 79 | # Node.js version, including v6, etc. 80 | 81 | # additional_hostnames: 82 | # - somename 83 | # - someothername 84 | # would provide http and https URLs for "somename.ddev.site" 85 | # and "someothername.ddev.site". 86 | 87 | # additional_fqdns: 88 | # - example.com 89 | # - sub1.example.com 90 | # would provide http and https URLs for "example.com" and "sub1.example.com" 91 | # Please take care with this because it can cause great confusion. 92 | 93 | # upload_dir: custom/upload/dir 94 | # would set the destination path for ddev import-files to /custom/upload/dir 95 | 96 | # working_dir: 97 | # web: /var/www/html 98 | # db: /home 99 | # would set the default working directory for the web and db services. 100 | # These values specify the destination directory for ddev ssh and the 101 | # directory in which commands passed into ddev exec are run. 102 | 103 | # omit_containers: [db, dba, ddev-ssh-agent] 104 | # Currently only these containers are supported. Some containers can also be 105 | # omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit 106 | # the "db" container, several standard features of ddev that access the 107 | # database container will be unusable. In the global configuration it is also 108 | # possible to omit ddev-router, but not here. 109 | 110 | # nfs_mount_enabled: false 111 | # Great performance improvement but requires host configuration first. 112 | # See https://ddev.readthedocs.io/en/stable/users/performance/#using-nfs-to-mount-the-project-into-the-container 113 | 114 | # mutagen_enabled: false 115 | # Experimental performance improvement using mutagen asynchronous updates. 116 | # See https://ddev.readthedocs.io/en/latest/users/performance/#using-mutagen 117 | 118 | # fail_on_hook_fail: False 119 | # Decide whether 'ddev start' should be interrupted by a failing hook 120 | 121 | # host_https_port: "59002" 122 | # The host port binding for https can be explicitly specified. It is 123 | # dynamic unless otherwise specified. 124 | # This is not used by most people, most people use the *router* instead 125 | # of the localhost port. 126 | 127 | # host_webserver_port: "59001" 128 | # The host port binding for the ddev-webserver can be explicitly specified. It is 129 | # dynamic unless otherwise specified. 130 | # This is not used by most people, most people use the *router* instead 131 | # of the localhost port. 132 | 133 | # host_db_port: "59002" 134 | # The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic 135 | # unless explicitly specified. 136 | 137 | # phpmyadmin_port: "8036" 138 | # phpmyadmin_https_port: "8037" 139 | # The PHPMyAdmin ports can be changed from the default 8036 and 8037 140 | 141 | # host_phpmyadmin_port: "8036" 142 | # The phpmyadmin (dba) port is not normally bound on the host at all, instead being routed 143 | # through ddev-router, but it can be specified and bound. 144 | 145 | # mailhog_port: "8025" 146 | # mailhog_https_port: "8026" 147 | # The MailHog ports can be changed from the default 8025 and 8026 148 | 149 | # host_mailhog_port: "8025" 150 | # The mailhog port is not normally bound on the host at all, instead being routed 151 | # through ddev-router, but it can be bound directly to localhost if specified here. 152 | 153 | # webimage_extra_packages: [php7.4-tidy, php-bcmath] 154 | # Extra Debian packages that are needed in the webimage can be added here 155 | 156 | # dbimage_extra_packages: [telnet,netcat] 157 | # Extra Debian packages that are needed in the dbimage can be added here 158 | 159 | # use_dns_when_possible: true 160 | # If the host has internet access and the domain configured can 161 | # successfully be looked up, DNS will be used for hostname resolution 162 | # instead of editing /etc/hosts 163 | # Defaults to true 164 | 165 | # project_tld: ddev.site 166 | # The top-level domain used for project URLs 167 | # The default "ddev.site" allows DNS lookup via a wildcard 168 | # If you prefer you can change this to "ddev.local" to preserve 169 | # pre-v1.9 behavior. 170 | 171 | # ngrok_args: --subdomain mysite --auth username:pass 172 | # Provide extra flags to the "ngrok http" command, see 173 | # https://ngrok.com/docs#http or run "ngrok http -h" 174 | 175 | # disable_settings_management: false 176 | # If true, ddev will not create CMS-specific settings files like 177 | # Drupal's settings.php/settings.ddev.php or TYPO3's AdditionalConfiguration.php 178 | # In this case the user must provide all such settings. 179 | 180 | # You can inject environment variables into the web container with: 181 | # web_environment: 182 | # - SOMEENV=somevalue 183 | # - SOMEOTHERENV=someothervalue 184 | 185 | # no_project_mount: false 186 | # (Experimental) If true, ddev will not mount the project into the web container; 187 | # the user is responsible for mounting it manually or via a script. 188 | # This is to enable experimentation with alternate file mounting strategies. 189 | # For advanced users only! 190 | 191 | # bind_all_interfaces: false 192 | # If true, host ports will be bound on all network interfaces, 193 | # not just the localhost interface. This means that ports 194 | # will be available on the local network if the host firewall 195 | # allows it. 196 | 197 | # Many ddev commands can be extended to run tasks before or after the 198 | # ddev command is executed, for example "post-start", "post-import-db", 199 | # "pre-composer", "post-composer" 200 | # See https://ddev.readthedocs.io/en/stable/users/extending-commands/ for more 201 | # information on the commands that can be extended and the tasks you can define 202 | # for them. Example: 203 | #hooks: 204 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | src/@types/ 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "i18n-text/no-en": "off", 12 | "eslint-comments/no-use": "off", 13 | "import/no-namespace": "off", 14 | "no-unused-vars": "off", 15 | "@typescript-eslint/no-unused-vars": "error", 16 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 17 | "@typescript-eslint/no-require-imports": "error", 18 | "@typescript-eslint/array-type": "error", 19 | "@typescript-eslint/await-thenable": "error", 20 | "@typescript-eslint/ban-ts-comment": "error", 21 | "camelcase": "off", 22 | "@typescript-eslint/consistent-type-assertions": "error", 23 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 24 | "@typescript-eslint/func-call-spacing": ["error", "never"], 25 | "@typescript-eslint/no-array-constructor": "error", 26 | "@typescript-eslint/no-empty-interface": "error", 27 | "@typescript-eslint/no-explicit-any": "error", 28 | "@typescript-eslint/no-extraneous-class": "error", 29 | "@typescript-eslint/no-for-in-array": "error", 30 | "@typescript-eslint/no-inferrable-types": "error", 31 | "@typescript-eslint/no-misused-new": "error", 32 | "@typescript-eslint/no-namespace": "error", 33 | "@typescript-eslint/no-non-null-assertion": "warn", 34 | "@typescript-eslint/no-unnecessary-qualifier": "error", 35 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 36 | "@typescript-eslint/no-useless-constructor": "error", 37 | "@typescript-eslint/no-var-requires": "error", 38 | "@typescript-eslint/prefer-for-of": "warn", 39 | "@typescript-eslint/prefer-function-type": "warn", 40 | "@typescript-eslint/prefer-includes": "error", 41 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 42 | "@typescript-eslint/promise-function-async": "error", 43 | "@typescript-eslint/require-array-sort-compare": "error", 44 | "@typescript-eslint/restrict-plus-operands": "error", 45 | "semi": "off", 46 | "@typescript-eslint/semi": ["error", "never"], 47 | "@typescript-eslint/type-annotation-spacing": "error", 48 | "@typescript-eslint/unbound-method": "error" 49 | }, 50 | "env": { 51 | "node": true, 52 | "es6": true, 53 | "jest/globals": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce checkout with linux lf consistent over all plattforms 2 | *.js text eol=lf 3 | *.json text eol=lf 4 | *.ts text eol=lf 5 | *.yml text eol=lf 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: gilbertsoft 2 | custom: https://paypal.me/SGilli 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Create a report to help us improve. 3 | labels: [bug] 4 | assignees: 5 | - gilbertsoft 6 | body: 7 | - type: checkboxes 8 | id: terms 9 | attributes: 10 | label: Code of Conduct 11 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GsActions/commit-message-checker/blob/main/CODE_OF_CONDUCT.md). 12 | options: 13 | - label: I agree to follow this project's Code of Conduct 14 | required: true 15 | - type: checkboxes 16 | attributes: 17 | label: Is there an existing issue for this? 18 | description: Please search to see if an issue already exists for the bug you encountered. 19 | options: 20 | - label: I have searched the existing issues 21 | required: true 22 | - type: dropdown 23 | id: gh-runner 24 | attributes: 25 | label: GitHub-hosted runner 26 | description: What GitHub-hosted runner are you using? 27 | multiple: false 28 | options: 29 | - ubuntu-latest 30 | - ubuntu-20.04 31 | - ubuntu-18.04 32 | - windows-latest 33 | - windows-2022 34 | - windows-2019 35 | - windows-2016 36 | - macos-latest 37 | - macos-11 38 | - macos-10.15 39 | - Other (please describe below) 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: other-runner 44 | attributes: 45 | label: Additional runner information 46 | description: Please provide more information about the used runner if you selected `Other` above. 47 | validations: 48 | required: false 49 | - type: textarea 50 | attributes: 51 | label: Current Behavior 52 | description: A concise description of what you're experiencing. 53 | validations: 54 | required: true 55 | - type: textarea 56 | attributes: 57 | label: Expected Behavior 58 | description: A concise description of what you expected to happen. 59 | validations: 60 | required: true 61 | - type: textarea 62 | attributes: 63 | label: Steps To Reproduce 64 | description: Steps to reproduce the behavior. 65 | placeholder: | 66 | 1. In this environment... 67 | 2. With this config... 68 | 3. Run '...' 69 | 4. See error... 70 | validations: 71 | required: false 72 | - type: textarea 73 | attributes: 74 | label: Anything else? 75 | description: | 76 | Links? References? Anything that will give us more context about the issue you are encountering! 77 | 78 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 79 | validations: 80 | required: false 81 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🚑 Community support 4 | url: https://github.com/GsActions/commit-message-checker/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest an idea for this project. 3 | labels: [enhancement] 4 | assignees: 5 | - gilbertsoft 6 | body: 7 | - type: checkboxes 8 | id: terms 9 | attributes: 10 | label: Code of Conduct 11 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GsActions/commit-message-checker/blob/main/CODE_OF_CONDUCT.md). 12 | options: 13 | - label: I agree to follow this project's Code of Conduct 14 | required: true 15 | - type: checkboxes 16 | attributes: 17 | label: Is there an existing issue for this? 18 | description: Please search to see if an issue already exists for the feature you request. 19 | options: 20 | - label: I have searched the existing issues 21 | required: true 22 | - type: checkboxes 23 | id: sponsoring 24 | attributes: 25 | label: Are you willing to sponsor your idea? 26 | description: Supporting a proposal can speed up the development of your idea and you can also better influence the final solution. 27 | options: 28 | - label: Yes, I like to sponsor this request 29 | required: false 30 | - type: textarea 31 | attributes: 32 | label: Is your feature request related to a problem? Please describe 33 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when... 34 | validations: 35 | required: true 36 | - type: textarea 37 | attributes: 38 | label: Describe the solution you'd like 39 | description: A clear and concise description of what you want to happen. 40 | validations: 41 | required: true 42 | - type: textarea 43 | attributes: 44 | label: Describe alternatives you've considered 45 | description: A clear and concise description of any alternative solutions or features you've considered. 46 | validations: 47 | required: false 48 | - type: textarea 49 | attributes: 50 | label: Additional context 51 | description: Add any other context or screenshots about the feature request here. 52 | validations: 53 | required: false 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "build-test" 2 | on: # rebuild any PRs and main branch changes 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | - 'releases/*' 8 | 9 | jobs: 10 | build: # make sure build/ci work properly 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set Node.js 16.x 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 16.x 18 | - name: Install 19 | run: | 20 | npm install 21 | - name: Build and Test 22 | run: | 23 | npm run all 24 | - name: Verify dist files 25 | run: | 26 | git diff 27 | # This fails when dist files are not up to date 28 | test -z "$(git diff --shortstat 2> /dev/null | tail -n1)" 29 | test: # make sure the action works on a clean machine without building 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: ./ 34 | with: 35 | pattern: '\[(BUGFIX|DOCS|FEATURE|TASK)\] .+$' 36 | error: 'Your first line has to contain a commit type like "[BUGFIX]".' 37 | checkAllCommitMessages: 'true' 38 | accessToken: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* 100 | 101 | # Ignore IDE related files and folders 102 | .idea 103 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | coc@gilbertsoft.org. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2020 Gilbertsoft LLC (gilbertsoft.org) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GS Commit Message Checker 2 | 3 | ![Version](https://img.shields.io/github/v/release/gsactions/commit-message-checker?style=flat-square) 4 | ![Test](https://github.com/gsactions/commit-message-checker/workflows/build-test/badge.svg) 5 | 6 | A GitHub action that checks that commit messages match a regex pattern. The 7 | action is able to act on pull request and push events and check the pull 8 | request title and body, or the commit message of the commits of a push. 9 | 10 | On pull requests the title and body are concatenated, delimited by two line 11 | breaks. 12 | 13 | Designed to be very flexible in usage, you can split checks into various 14 | workflows, use action types on pull request to listen on, define branches 15 | for pushes etc. etc. 16 | 17 | ## Configuration 18 | 19 | See also [action definition](action.yml) and the following example workflow. 20 | 21 | More information about `pattern` and `flags` can be found in the 22 | [JavaScript reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp). 23 | 24 | `flags` is optional and defaults to `gm`. 25 | 26 | `excludeDescription`, `excludeTitle` and `checkAllCommitMessages` are optional. 27 | Default behavior is to include the description and title and not check pull 28 | request commit messages. 29 | 30 | ### Example Workflow 31 | 32 | ```yml 33 | name: 'Commit Message Check' 34 | on: 35 | pull_request: 36 | types: 37 | - opened 38 | - edited 39 | - reopened 40 | - synchronize 41 | pull_request_target: 42 | types: 43 | - opened 44 | - edited 45 | - reopened 46 | - synchronize 47 | push: 48 | branches: 49 | - main 50 | - 'releases/*' 51 | 52 | jobs: 53 | check-commit-message: 54 | name: Check Commit Message 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Check Commit Type 58 | uses: gsactions/commit-message-checker@v2 59 | with: 60 | pattern: '\[[^]]+\] .+$' 61 | flags: 'gm' 62 | error: 'Your first line has to contain a commit type like "[BUGFIX]".' 63 | - name: Check Line Length 64 | uses: gsactions/commit-message-checker@v2 65 | with: 66 | pattern: '^[^#].{74}' 67 | error: 'The maximum line length of 74 characters is exceeded.' 68 | excludeDescription: 'true' # optional: this excludes the description body of a pull request 69 | excludeTitle: 'true' # optional: this excludes the title of a pull request 70 | checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request 71 | accessToken: ${{ secrets.GITHUB_TOKEN }} # github access token is only required if checkAllCommitMessages is true 72 | - name: Check for Resolves / Fixes 73 | uses: gsactions/commit-message-checker@v2 74 | with: 75 | pattern: '^.+(Resolves|Fixes): \#[0-9]+$' 76 | error: 'You need at least one "Resolves|Fixes: #" line.' 77 | ``` 78 | 79 | ### Troubleshooting and debugging 80 | 81 | Most of the questions being asked here are not about bugs or missing features in 82 | this action, but about not enough information about what is going on in the 83 | background. A good first starting point is to [enable debug logging](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging) 84 | for the action, which can be accomplished by adding secrets to your repository. 85 | After that, many additional information will appear in the logs and you should 86 | be able to set up your configuration properly. 87 | 88 | There are some really good tools that you can use to set up your pattern 89 | properly for your needs. My favorite tool is which works 90 | very well with this action. 91 | 92 | If you need additional support, please head to the [GitHub Discussions](https://github.com/GsActions/commit-message-checker/discussions) 93 | of this repository. 94 | 95 | ## Development 96 | 97 | ### Quick Start 98 | 99 | ```sh 100 | git clone https://github.com/gsactions/commit-message-checker.git 101 | npm install 102 | npm run build 103 | ``` 104 | 105 | That's it, just start editing the sources... 106 | 107 | ### Commands 108 | 109 | Below is a list of commands you will probably find useful during the development 110 | cycle. 111 | 112 | #### `npm run build` 113 | 114 | Builds the package to the `lib` folder. 115 | 116 | #### `npm run format` 117 | 118 | Runs Prettier on .ts and .tsx files and fixes errors. 119 | 120 | #### `npm run format-check` 121 | 122 | Runs Prettier on .ts and .tsx files without fixing errors. 123 | 124 | #### `npm run lint` 125 | 126 | Runs Eslint on .ts and .tsx files. 127 | 128 | #### `npm run pack` 129 | 130 | Bundles the package to the `dist` folder. 131 | 132 | #### `npm run test` 133 | 134 | Runs Jest test suites. 135 | 136 | #### `npm run all` 137 | 138 | Runs all the above commands. 139 | 140 | ### Debugging 141 | 142 | More information about debugging GitHub Actions can be found at . 143 | 144 | The secrets `ACTIONS_STEP_DEBUG` and `ACTIONS_RUNNER_DEBUG` are both set to 145 | `true` in the main repository. 146 | 147 | ## License 148 | 149 | This project is released under the terms of the [MIT License](LICENSE) 150 | -------------------------------------------------------------------------------- /__tests__/commit-message-checker.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the "GS Commit Message Checker" Action for Github. 3 | * 4 | * Copyright (C) 2019 by Gilbertsoft LLC (gilbertsoft.org) 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 9 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 11 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 12 | * THE SOFTWARE. 13 | * 14 | * For the full license information, please read the LICENSE file that 15 | * was distributed with this source code. 16 | */ 17 | 18 | /** 19 | * Imports 20 | */ 21 | import {ICheckerArguments} from '../src/commit-message-checker' 22 | 23 | // Late bind 24 | let commitMessageChecker: any 25 | 26 | describe('commit-message-checker tests', () => { 27 | beforeAll(() => { 28 | // Now import 29 | commitMessageChecker = require('../lib/commit-message-checker') 30 | }) 31 | 32 | it('requires pattern', async () => { 33 | const checkerArguments: ICheckerArguments = { 34 | pattern: '', 35 | flags: '', 36 | error: '', 37 | messages: [] 38 | } 39 | await expect( 40 | commitMessageChecker.checkCommitMessages(checkerArguments) 41 | ).rejects.toThrow('PATTERN not defined.') 42 | }) 43 | 44 | it('requires valid flags', async () => { 45 | const checkerArguments: ICheckerArguments = { 46 | pattern: 'some-pattern', 47 | flags: 'abcdefgh', 48 | error: '', 49 | messages: [] 50 | } 51 | await expect( 52 | commitMessageChecker.checkCommitMessages(checkerArguments) 53 | ).rejects.toThrow('FLAGS contains invalid characters "abcdefh".') 54 | }) 55 | 56 | it('requires error message', async () => { 57 | const checkerArguments: ICheckerArguments = { 58 | pattern: 'some-pattern', 59 | flags: '', 60 | error: '', 61 | messages: [] 62 | } 63 | await expect( 64 | commitMessageChecker.checkCommitMessages(checkerArguments) 65 | ).rejects.toThrow('ERROR not defined.') 66 | }) 67 | 68 | it('requires messages', async () => { 69 | const checkerArguments: ICheckerArguments = { 70 | pattern: 'some-pattern', 71 | flags: '', 72 | error: 'some-error', 73 | messages: [] 74 | } 75 | await expect( 76 | commitMessageChecker.checkCommitMessages(checkerArguments) 77 | ).rejects.toThrow('MESSAGES not defined.') 78 | }) 79 | 80 | it('check fails single message', async () => { 81 | const checkerArguments: ICheckerArguments = { 82 | pattern: 'some-pattern', 83 | flags: '', 84 | error: 'some-error', 85 | messages: ['some-message'] 86 | } 87 | await expect( 88 | commitMessageChecker.checkCommitMessages(checkerArguments) 89 | ).rejects.toThrow('some-error') 90 | }) 91 | 92 | it('check fails multiple messages', async () => { 93 | const checkerArguments: ICheckerArguments = { 94 | pattern: 'some-pattern', 95 | flags: '', 96 | error: 'some-error', 97 | messages: ['some-message', 'some-pattern'] 98 | } 99 | await expect( 100 | commitMessageChecker.checkCommitMessages(checkerArguments) 101 | ).rejects.toThrow('some-error') 102 | }) 103 | 104 | it('check succeeds on single message', async () => { 105 | const checkerArguments: ICheckerArguments = { 106 | pattern: '.*', 107 | flags: '', 108 | error: 'some-error', 109 | messages: ['some-message'] 110 | } 111 | await expect( 112 | commitMessageChecker.checkCommitMessages(checkerArguments) 113 | ).resolves.toBeUndefined() 114 | }) 115 | 116 | it('check succeeds on multiple messages', async () => { 117 | const checkerArguments: ICheckerArguments = { 118 | pattern: '.*', 119 | flags: '', 120 | error: 'some-error', 121 | messages: ['some-message', 'other-message'] 122 | } 123 | await expect( 124 | commitMessageChecker.checkCommitMessages(checkerArguments) 125 | ).resolves.toBeUndefined() 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /__tests__/input-helper.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the "GS Commit Message Checker" Action for Github. 3 | * 4 | * Copyright (C) 2019 by Gilbertsoft LLC (gilbertsoft.org) 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 9 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 11 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 12 | * THE SOFTWARE. 13 | * 14 | * For the full license information, please read the LICENSE file that 15 | * was distributed with this source code. 16 | */ 17 | 18 | /** 19 | * Imports 20 | */ 21 | import {ICheckerArguments} from '../src/commit-message-checker' 22 | import {InputOptions} from '@actions/core' 23 | 24 | // Late bind 25 | let inputHelper: any 26 | 27 | // Mock @actions/core 28 | let inputs = {} as any 29 | const mockCore = jest.genMockFromModule('@actions/core') as any 30 | mockCore.getInput = (name: string, options?: InputOptions) => { 31 | const val = inputs[name] || '' 32 | if (options && options.required && !val) { 33 | throw new Error(`Input required and not supplied: ${name}`) 34 | } 35 | return val.trim() 36 | } 37 | 38 | // Mock @actions/github 39 | const mockGitHub = jest.genMockFromModule('@actions/github') as any 40 | mockGitHub.context = {} 41 | 42 | // Mock @octokit/graphql 43 | let graphqlResponse = {} as any 44 | const mockGraphql = jest.genMockFromModule('@octokit/graphql') as any 45 | mockGraphql.graphql = (query: string, parameters?: any): any => { 46 | return graphqlResponse 47 | } 48 | 49 | describe('input-helper tests', () => { 50 | beforeAll(() => { 51 | // Mocks 52 | jest.setMock('@actions/core', mockCore) 53 | jest.setMock('@actions/github', mockGitHub) 54 | jest.setMock('@octokit/graphql', mockGraphql) 55 | 56 | // Now import 57 | inputHelper = require('../lib/input-helper') 58 | }) 59 | 60 | beforeEach(() => { 61 | // Reset inputs and context 62 | inputs = {} 63 | mockGitHub.context = {} 64 | graphqlResponse = {} 65 | }) 66 | 67 | afterAll(() => { 68 | // Reset modules 69 | jest.resetModules() 70 | }) 71 | 72 | it('requires pattern', async () => { 73 | await expect(inputHelper.getInputs()).rejects.toThrow( 74 | 'Input required and not supplied: pattern' 75 | ) 76 | }) 77 | 78 | it('requires error message', async () => { 79 | inputs.pattern = 'some-pattern' 80 | await expect(inputHelper.getInputs()).rejects.toThrow( 81 | 'Input required and not supplied: error' 82 | ) 83 | }) 84 | 85 | it('requires event', async () => { 86 | inputs.pattern = 'some-pattern' 87 | inputs.error = 'some-error' 88 | await expect(inputHelper.getInputs()).rejects.toThrow( 89 | 'Event "undefined" is not supported.' 90 | ) 91 | }) 92 | 93 | it('requires valid event', async () => { 94 | mockGitHub.context = { 95 | eventName: 'some-event' 96 | } 97 | inputs.pattern = 'some-pattern' 98 | inputs.error = 'some-error' 99 | await expect(inputHelper.getInputs()).rejects.toThrow( 100 | 'Event "some-event" is not supported.' 101 | ) 102 | }) 103 | 104 | it('sets pattern', async () => { 105 | mockGitHub.context = { 106 | eventName: 'pull_request', 107 | payload: { 108 | pull_request: { 109 | title: 'some-title', 110 | body: '' 111 | } 112 | } 113 | } 114 | inputs.pattern = 'some-pattern' 115 | inputs.flags = 'abcdefgh' 116 | inputs.error = 'some-error' 117 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 118 | expect(checkerArguments.pattern).toBe('some-pattern') 119 | }) 120 | 121 | it('sets flags', async () => { 122 | mockGitHub.context = { 123 | eventName: 'pull_request', 124 | payload: { 125 | pull_request: { 126 | title: 'some-title', 127 | body: '' 128 | } 129 | } 130 | } 131 | inputs.pattern = 'some-pattern' 132 | inputs.flags = 'abcdefgh' 133 | inputs.error = 'some-error' 134 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 135 | expect(checkerArguments.flags).toBe('abcdefgh') 136 | }) 137 | 138 | it('sets error', async () => { 139 | mockGitHub.context = { 140 | eventName: 'pull_request', 141 | payload: { 142 | pull_request: { 143 | title: 'some-title', 144 | body: '' 145 | } 146 | } 147 | } 148 | inputs.pattern = 'some-pattern' 149 | inputs.flags = 'abcdefgh' 150 | inputs.error = 'some-error' 151 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 152 | expect(checkerArguments.error).toBe('some-error') 153 | }) 154 | 155 | it('requires pull_request payload', async () => { 156 | mockGitHub.context = { 157 | eventName: 'pull_request' 158 | } 159 | inputs.pattern = 'some-pattern' 160 | inputs.error = 'some-error' 161 | await expect(inputHelper.getInputs()).rejects.toThrow( 162 | 'No payload found in the context.' 163 | ) 164 | }) 165 | 166 | it('requires pull_request', async () => { 167 | mockGitHub.context = { 168 | eventName: 'pull_request', 169 | payload: {} 170 | } 171 | inputs.pattern = 'some-pattern' 172 | inputs.error = 'some-error' 173 | await expect(inputHelper.getInputs()).rejects.toThrow( 174 | 'No pull_request found in the payload.' 175 | ) 176 | }) 177 | 178 | it('requires pull_request title', async () => { 179 | mockGitHub.context = { 180 | eventName: 'pull_request', 181 | payload: { 182 | pull_request: { 183 | title: '', 184 | body: '' 185 | } 186 | } 187 | } 188 | inputs.pattern = 'some-pattern' 189 | inputs.error = 'some-error' 190 | await expect(inputHelper.getInputs()).rejects.toThrow( 191 | 'No title found in the pull_request.' 192 | ) 193 | }) 194 | 195 | it('sets pull_request title', async () => { 196 | mockGitHub.context = { 197 | eventName: 'pull_request', 198 | payload: { 199 | pull_request: { 200 | title: 'some-title', 201 | body: '' 202 | } 203 | } 204 | } 205 | inputs.pattern = 'some-pattern' 206 | inputs.error = 'some-error' 207 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 208 | expect(checkerArguments).toBeTruthy() 209 | expect(checkerArguments.messages).toBeTruthy() 210 | expect(checkerArguments.messages[0]).toBe('some-title') 211 | }) 212 | 213 | it('sets pull_request title and body', async () => { 214 | mockGitHub.context = { 215 | eventName: 'pull_request', 216 | payload: { 217 | pull_request: { 218 | title: 'some-title', 219 | body: 'some-body' 220 | } 221 | } 222 | } 223 | inputs.pattern = 'some-pattern' 224 | inputs.error = 'some-error' 225 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 226 | expect(checkerArguments).toBeTruthy() 227 | expect(checkerArguments.messages).toBeTruthy() 228 | expect(checkerArguments.messages[0]).toBe('some-title\n\nsome-body') 229 | }) 230 | 231 | it('excludes pull_request body', async () => { 232 | mockGitHub.context = { 233 | eventName: 'pull_request', 234 | payload: { 235 | pull_request: { 236 | title: 'some-title', 237 | body: 'some-body' 238 | } 239 | } 240 | } 241 | inputs.pattern = 'some-pattern' 242 | inputs.error = 'some-error' 243 | inputs.excludeDescription = 'true' 244 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 245 | expect(checkerArguments).toBeTruthy() 246 | expect(checkerArguments.messages).toBeTruthy() 247 | expect(checkerArguments.messages[0]).toBe('some-title') 248 | }) 249 | 250 | it('excludes pull_request title', async () => { 251 | mockGitHub.context = { 252 | eventName: 'pull_request', 253 | payload: { 254 | pull_request: { 255 | title: 'some-title', 256 | body: 'some-body' 257 | } 258 | } 259 | } 260 | inputs.pattern = 'some-pattern' 261 | inputs.error = 'some-error' 262 | inputs.excludeTitle = 'true' 263 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 264 | expect(checkerArguments).toBeTruthy() 265 | expect(checkerArguments.messages).toBeTruthy() 266 | expect(checkerArguments.messages[0]).toBe('some-body') 267 | }) 268 | 269 | it('excludes pull_request title and body', async () => { 270 | mockGitHub.context = { 271 | eventName: 'pull_request', 272 | payload: { 273 | pull_request: { 274 | title: 'some-title', 275 | body: 'some-body' 276 | } 277 | } 278 | } 279 | inputs.pattern = 'some-pattern' 280 | inputs.error = 'some-error' 281 | inputs.excludeDescription = 'true' 282 | inputs.excludeTitle = 'true' 283 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 284 | expect(checkerArguments).toBeTruthy() 285 | expect(checkerArguments.messages).toBeTruthy() 286 | expect(checkerArguments.messages.length).toBe(0) 287 | }) 288 | 289 | it('requires accessToken', async () => { 290 | mockGitHub.context = { 291 | eventName: 'pull_request', 292 | payload: { 293 | pull_request: { 294 | title: 'some-title', 295 | body: '' 296 | } 297 | } 298 | } 299 | inputs.pattern = 'some-pattern' 300 | inputs.error = 'some-error' 301 | inputs.checkAllCommitMessages = 'true' 302 | await expect(inputHelper.getInputs()).rejects.toThrow( 303 | 'The `checkAllCommitMessages` option requires a github access token.' 304 | ) 305 | }) 306 | 307 | it('requires pull_request number', async () => { 308 | mockGitHub.context = { 309 | eventName: 'pull_request', 310 | payload: { 311 | pull_request: { 312 | title: 'some-title', 313 | body: '' 314 | } 315 | } 316 | } 317 | inputs.pattern = 'some-pattern' 318 | inputs.error = 'some-error' 319 | inputs.checkAllCommitMessages = 'true' 320 | inputs.accessToken = 'dummy-token' 321 | await expect(inputHelper.getInputs()).rejects.toThrow( 322 | 'No number found in the pull_request.' 323 | ) 324 | }) 325 | 326 | it('requires repository', async () => { 327 | mockGitHub.context = { 328 | eventName: 'pull_request', 329 | payload: { 330 | pull_request: { 331 | title: 'some-title', 332 | body: '', 333 | number: 12345 334 | } 335 | } 336 | } 337 | inputs.pattern = 'some-pattern' 338 | inputs.error = 'some-error' 339 | inputs.checkAllCommitMessages = 'true' 340 | inputs.accessToken = 'dummy-token' 341 | await expect(inputHelper.getInputs()).rejects.toThrow( 342 | 'No repository found in the payload.' 343 | ) 344 | }) 345 | 346 | it('requires repository name', async () => { 347 | mockGitHub.context = { 348 | eventName: 'pull_request', 349 | payload: { 350 | pull_request: { 351 | title: 'some-title', 352 | body: '', 353 | number: 12345 354 | }, 355 | repository: {} 356 | } 357 | } 358 | inputs.pattern = 'some-pattern' 359 | inputs.error = 'some-error' 360 | inputs.checkAllCommitMessages = 'true' 361 | inputs.accessToken = 'dummy-token' 362 | await expect(inputHelper.getInputs()).rejects.toThrow( 363 | 'No name found in the repository.' 364 | ) 365 | }) 366 | 367 | it('requires repository owner (1)', async () => { 368 | mockGitHub.context = { 369 | eventName: 'pull_request', 370 | payload: { 371 | pull_request: { 372 | title: 'some-title', 373 | body: '', 374 | number: 12345 375 | }, 376 | repository: { 377 | name: 'repository-name' 378 | } 379 | } 380 | } 381 | inputs.pattern = 'some-pattern' 382 | inputs.error = 'some-error' 383 | inputs.checkAllCommitMessages = 'true' 384 | inputs.accessToken = 'dummy-token' 385 | await expect(inputHelper.getInputs()).rejects.toThrow( 386 | 'No owner found in the repository.' 387 | ) 388 | }) 389 | 390 | it('requires repository owner (2)', async () => { 391 | mockGitHub.context = { 392 | eventName: 'pull_request', 393 | payload: { 394 | pull_request: { 395 | title: 'some-title', 396 | body: '', 397 | number: 12345 398 | }, 399 | repository: { 400 | name: 'repository-name', 401 | owner: {} 402 | } 403 | } 404 | } 405 | inputs.pattern = 'some-pattern' 406 | inputs.error = 'some-error' 407 | inputs.checkAllCommitMessages = 'true' 408 | inputs.accessToken = 'dummy-token' 409 | await expect(inputHelper.getInputs()).rejects.toThrow( 410 | 'No owner found in the repository.' 411 | ) 412 | }) 413 | 414 | it('sets pull_request commits', async () => { 415 | mockGitHub.context = { 416 | eventName: 'pull_request', 417 | payload: { 418 | pull_request: { 419 | title: 'some-title', 420 | body: 'some-body', 421 | number: 1 422 | }, 423 | repository: { 424 | owner: { 425 | name: 'some-owner' 426 | }, 427 | name: 'some-repo' 428 | } 429 | } 430 | } 431 | 432 | inputs.pattern = 'some-pattern' 433 | inputs.error = 'some-error' 434 | inputs.excludeDescription = 'true' 435 | inputs.excludeTitle = 'true' 436 | inputs.checkAllCommitMessages = 'true' 437 | inputs.accessToken = 'some-token' 438 | 439 | const response = { 440 | repository: { 441 | pullRequest: { 442 | commits: { 443 | edges: [ 444 | { 445 | node: { 446 | commit: { 447 | message: 448 | 'input: make input-helper functions async\n\nIn order to work with asynchronous call like an async http request\nin an easier way, the functions getInput and getMessages were\nconverted to async.' 449 | } 450 | } 451 | }, 452 | { 453 | node: { 454 | commit: { 455 | message: 456 | "input: PR options ignore title and check PR commits\n\nthis make it possible to igore partially or completely the PR payload.\nThe commits associated with the pull request can be checked instead of\nchecking the pull request payload. The parameter are:\n\n- excludeTitle: 'true | false'\n- excludeDescription: 'true | false'\n- checkAllCommitMessages: 'true | false'\n\nby default, all options comes false." 457 | } 458 | } 459 | }, 460 | { 461 | node: { 462 | commit: { 463 | message: 464 | 'docs: include parameters excludeTitle, checkAllCommitMessages and accessToken\n\nCo-authored-by: Gilbertsoft <25326036+gilbertsoft@users.noreply.github.com>' 465 | } 466 | } 467 | } 468 | ] 469 | } 470 | } 471 | } 472 | } 473 | 474 | graphqlResponse = response 475 | 476 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 477 | expect(checkerArguments).toBeTruthy() 478 | expect(checkerArguments.pattern).toBe('some-pattern') 479 | expect(checkerArguments.error).toBe('some-error') 480 | expect(checkerArguments.messages).toBeTruthy() 481 | expect(checkerArguments.messages.length).toBe(3) 482 | }) 483 | 484 | it('require push payload', async () => { 485 | mockGitHub.context = { 486 | eventName: 'push' 487 | } 488 | inputs.pattern = 'some-pattern' 489 | inputs.error = 'some-error' 490 | await expect(inputHelper.getInputs()).rejects.toThrow( 491 | 'No payload found in the context.' 492 | ) 493 | }) 494 | 495 | it('push payload is optional', async () => { 496 | mockGitHub.context = { 497 | eventName: 'push', 498 | payload: {} 499 | } 500 | inputs.pattern = 'some-pattern' 501 | inputs.error = 'some-error' 502 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 503 | expect(checkerArguments.messages).toHaveLength(0) 504 | }) 505 | 506 | it('push payload commits is optional', async () => { 507 | mockGitHub.context = { 508 | eventName: 'push', 509 | payload: { 510 | commits: {} 511 | } 512 | } 513 | inputs.pattern = 'some-pattern' 514 | inputs.error = 'some-error' 515 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 516 | expect(checkerArguments.messages).toHaveLength(0) 517 | }) 518 | 519 | it('sets correct single push payload', async () => { 520 | mockGitHub.context = { 521 | eventName: 'push', 522 | payload: { 523 | commits: [ 524 | { 525 | message: 'some-message' 526 | } 527 | ] 528 | } 529 | } 530 | inputs.pattern = 'some-pattern' 531 | inputs.error = 'some-error' 532 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 533 | expect(checkerArguments).toBeTruthy() 534 | expect(checkerArguments.pattern).toBe('some-pattern') 535 | expect(checkerArguments.error).toBe('some-error') 536 | expect(checkerArguments.messages).toBeTruthy() 537 | expect(checkerArguments.messages[0]).toBe('some-message') 538 | }) 539 | 540 | it('sets correct multiple push payload', async () => { 541 | mockGitHub.context = { 542 | eventName: 'push', 543 | payload: { 544 | commits: [ 545 | { 546 | message: 'some-message' 547 | }, 548 | { 549 | message: 'other-message' 550 | } 551 | ] 552 | } 553 | } 554 | inputs.pattern = 'some-pattern' 555 | inputs.error = 'some-error' 556 | const checkerArguments: ICheckerArguments = await inputHelper.getInputs() 557 | expect(checkerArguments).toBeTruthy() 558 | expect(checkerArguments.pattern).toBe('some-pattern') 559 | expect(checkerArguments.error).toBe('some-error') 560 | expect(checkerArguments.messages).toBeTruthy() 561 | expect(checkerArguments.messages[0]).toBe('some-message') 562 | expect(checkerArguments.messages[1]).toBe('other-message') 563 | }) 564 | }) 565 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'GS Commit Message Checker' 2 | description: 'Check the commit message against a regex pattern' 3 | author: 'Gilbertsoft LLC' 4 | inputs: 5 | pattern: 6 | description: 'A regex pattern to check if a commit message is valid.' 7 | required: true 8 | flags: 9 | description: 'Expression flags change how the expression is interpreted.' 10 | required: false 11 | default: 'gm' 12 | error: 13 | description: 'A error message which will be returned in case of an error.' 14 | required: true 15 | excludeTitle: 16 | description: 'Setting this input to true will exclude the Pull Request title from the check.' 17 | required: false 18 | default: 'false' 19 | excludeDescription: 20 | description: 'Setting this input to true will exclude the Pull Request description from the check.' 21 | required: false 22 | default: 'false' 23 | checkAllCommitMessages: 24 | description: 'Setting this input to true will check all Pull Request commits' 25 | required: false 26 | default: 'false' 27 | accessToken: 28 | description: 'you must provide GITHUB_TOKEN to this input if checkAllCommitMessages is true' 29 | required: false 30 | default: 'false' 31 | runs: 32 | using: node16 33 | main: dist/index.js 34 | branding: 35 | icon: 'check' 36 | color: 'blue' 37 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gsactions/commit-message-checker", 3 | "version": "2.0.0-dev.0", 4 | "description": "GitHub Action that checks commit messages of pushes and pull request against a regex pattern", 5 | "keywords": [ 6 | "github", 7 | "actions", 8 | "commit", 9 | "message", 10 | "pull", 11 | "request", 12 | "push" 13 | ], 14 | "homepage": "https://github.com/GsActions/commit-message-checker#readme", 15 | "bugs": { 16 | "url": "https://github.com/GsActions/commit-message-checker/issues" 17 | }, 18 | "license": "MIT", 19 | "author": "Simon Gilli (https://gilbertsoft.org)", 20 | "files": [ 21 | "action.yml", 22 | "dist", 23 | "lib" 24 | ], 25 | "main": "lib/main.js", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/GsActions/commit-message-checker.git" 29 | }, 30 | "scripts": { 31 | "build": "tsc", 32 | "format": "prettier --write '**/*.ts'", 33 | "format-check": "prettier --check '**/*.ts'", 34 | "lint": "eslint src/**/*.ts", 35 | "package": "ncc build", 36 | "test": "jest", 37 | "all": "npm run build && npm run format && npm run lint && npm run package && npm test" 38 | }, 39 | "dependencies": { 40 | "@actions/core": "^1.10.0", 41 | "@actions/github": "^5.1.1", 42 | "@octokit/graphql": "^5.0.3" 43 | }, 44 | "devDependencies": { 45 | "@tsconfig/node16": "^1.0.0", 46 | "@types/jest": "^29.2.0", 47 | "@types/node": "^18.11.0", 48 | "@types/node-fetch": "^2.5.10", 49 | "@typescript-eslint/parser": "^5.40.1", 50 | "@vercel/ncc": "^0.34.0", 51 | "eslint": "^8.25.0", 52 | "eslint-plugin-github": "^4.3.2", 53 | "eslint-plugin-jest": "^27.1.3", 54 | "jest": "^29.2.1", 55 | "js-yaml": "^4.1.0", 56 | "prettier": "2.7.1", 57 | "ts-jest": "^29.0.3", 58 | "typescript": "^4.4.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commit-message-checker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the "GS Commit Message Checker" Action for Github. 3 | * 4 | * Copyright (C) 2019-2022 by Gilbertsoft LLC (gilbertsoft.org) 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 9 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 11 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 12 | * THE SOFTWARE. 13 | * 14 | * For the full license information, please read the LICENSE file that 15 | * was distributed with this source code. 16 | */ 17 | 18 | /** 19 | * Imports 20 | */ 21 | import * as core from '@actions/core' 22 | 23 | /** 24 | * Interface used as arguments for the check function containing the pattern, 25 | * error message and the messages. 26 | */ 27 | export interface ICheckerArguments { 28 | pattern: string 29 | flags: string 30 | error: string 31 | messages: string[] 32 | } 33 | 34 | /** 35 | * Checks commit messages given by args. 36 | * 37 | * @param args messages, pattern and error message to process. 38 | * @returns void 39 | */ 40 | export async function checkCommitMessages( 41 | args: ICheckerArguments 42 | ): Promise { 43 | // Check arguments 44 | if (args.pattern.length === 0) { 45 | throw new Error(`PATTERN not defined.`) 46 | } 47 | 48 | const regex = new RegExp('[^gimsuy]', 'g') 49 | let invalidChars 50 | let chars = '' 51 | while ((invalidChars = regex.exec(args.flags)) !== null) { 52 | chars += invalidChars[0] 53 | } 54 | if (chars !== '') { 55 | throw new Error(`FLAGS contains invalid characters "${chars}".`) 56 | } 57 | 58 | if (args.error.length === 0) { 59 | throw new Error(`ERROR not defined.`) 60 | } 61 | 62 | if (args.messages.length === 0) { 63 | throw new Error(`MESSAGES not defined.`) 64 | } 65 | 66 | // Check messages 67 | let result = true 68 | 69 | core.info(`Checking commit messages against "${args.pattern}"...`) 70 | 71 | for (const message of args.messages) { 72 | if (checkMessage(message, args.pattern, args.flags)) { 73 | core.info(`- OK: "${message}"`) 74 | } else { 75 | core.info(`- failed: "${message}"`) 76 | result = false 77 | } 78 | } 79 | 80 | // Throw error in case of failed test 81 | if (!result) { 82 | throw new Error(args.error) 83 | } 84 | } 85 | 86 | /** 87 | * Checks the message against the regex pattern. 88 | * 89 | * @param message message to check against the pattern. 90 | * @param pattern regex pattern for the check. 91 | * @returns boolean 92 | */ 93 | function checkMessage( 94 | message: string, 95 | pattern: string, 96 | flags: string 97 | ): boolean { 98 | const regex = new RegExp(pattern, flags) 99 | return regex.test(message) 100 | } 101 | -------------------------------------------------------------------------------- /src/input-helper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the "GS Commit Message Checker" Action for Github. 3 | * 4 | * Copyright (C) 2019-2022 by Gilbertsoft LLC (gilbertsoft.org) 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 9 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 11 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 12 | * THE SOFTWARE. 13 | * 14 | * For the full license information, please read the LICENSE file that 15 | * was distributed with this source code. 16 | */ 17 | 18 | /** 19 | * Imports 20 | */ 21 | import * as core from '@actions/core' 22 | import * as github from '@actions/github' 23 | import {graphql} from '@octokit/graphql' 24 | import {ICheckerArguments} from './commit-message-checker' 25 | 26 | export interface PullRequestOptions { 27 | ignoreTitle: boolean 28 | ignoreDescription: boolean 29 | checkAllCommitMessages: boolean // requires github token 30 | accessToken: string 31 | } 32 | 33 | /** 34 | * Gets the inputs set by the user and the messages of the event. 35 | * 36 | * @returns ICheckerArguments 37 | */ 38 | export async function getInputs(): Promise { 39 | const result = {} as unknown as ICheckerArguments 40 | 41 | core.debug('Get inputs...') 42 | 43 | // Get pattern 44 | result.pattern = core.getInput('pattern', {required: true}) 45 | core.debug(`pattern: ${result.pattern}`) 46 | 47 | // Get flags 48 | result.flags = core.getInput('flags') 49 | core.debug(`flags: ${result.flags}`) 50 | 51 | // Get error message 52 | result.error = core.getInput('error', {required: true}) 53 | core.debug(`error: ${result.error}`) 54 | 55 | // Get excludeTitle 56 | const excludeTitleStr = core.getInput('excludeTitle') 57 | core.debug(`excludeTitle: ${excludeTitleStr}`) 58 | 59 | // Get excludeDescription 60 | const excludeDescriptionStr = core.getInput('excludeDescription') 61 | core.debug(`excludeDescription: ${excludeDescriptionStr}`) 62 | 63 | // Get checkAllCommitMessages 64 | const checkAllCommitMessagesStr = core.getInput('checkAllCommitMessages') 65 | core.debug(`checkAllCommitMessages: ${checkAllCommitMessagesStr}`) 66 | 67 | // Set pullRequestOptions 68 | const pullRequestOptions: PullRequestOptions = { 69 | ignoreTitle: excludeTitleStr 70 | ? excludeTitleStr === 'true' 71 | : /* default */ false, 72 | ignoreDescription: excludeDescriptionStr 73 | ? excludeDescriptionStr === 'true' 74 | : /* default */ false, 75 | checkAllCommitMessages: checkAllCommitMessagesStr 76 | ? checkAllCommitMessagesStr === 'true' 77 | : /* default */ false, 78 | accessToken: core.getInput('accessToken') 79 | } 80 | core.debug(`accessToken: ${pullRequestOptions.accessToken}`) 81 | 82 | // Get commit messages 83 | result.messages = await getMessages(pullRequestOptions) 84 | 85 | return result 86 | } 87 | 88 | /** 89 | * Gets all commit messages of a push or title and body of a pull request 90 | * concatenated to one message. 91 | * 92 | * @returns string[] 93 | */ 94 | async function getMessages( 95 | pullRequestOptions: PullRequestOptions 96 | ): Promise { 97 | core.debug('Get messages...') 98 | core.debug( 99 | ` - pullRequestOptions: ${JSON.stringify(pullRequestOptions, null, 2)}` 100 | ) 101 | 102 | const messages: string[] = [] 103 | 104 | core.debug(` - eventName: ${github.context.eventName}`) 105 | 106 | switch (github.context.eventName) { 107 | case 'pull_request_target': 108 | case 'pull_request': { 109 | if (!github.context.payload) { 110 | throw new Error('No payload found in the context.') 111 | } 112 | 113 | if (!github.context.payload.pull_request) { 114 | throw new Error('No pull_request found in the payload.') 115 | } 116 | 117 | let message = '' 118 | 119 | // Handle pull request title and body 120 | if (!pullRequestOptions.ignoreTitle) { 121 | if (!github.context.payload.pull_request.title) { 122 | throw new Error('No title found in the pull_request.') 123 | } 124 | 125 | message += github.context.payload.pull_request.title 126 | } else { 127 | core.debug(' - skipping title') 128 | } 129 | 130 | if (!pullRequestOptions.ignoreDescription) { 131 | if (github.context.payload.pull_request.body) { 132 | message = message.concat( 133 | message !== '' ? '\n\n' : '', 134 | github.context.payload.pull_request.body 135 | ) 136 | } 137 | } else { 138 | core.debug(' - skipping description') 139 | } 140 | 141 | if (message) { 142 | messages.push(message) 143 | } 144 | 145 | // Handle pull request commits 146 | if (pullRequestOptions.checkAllCommitMessages) { 147 | if (!pullRequestOptions.accessToken) { 148 | throw new Error( 149 | 'The `checkAllCommitMessages` option requires a github access token.' 150 | ) 151 | } 152 | 153 | if (!github.context.payload.pull_request.number) { 154 | throw new Error('No number found in the pull_request.') 155 | } 156 | 157 | if (!github.context.payload.repository) { 158 | throw new Error('No repository found in the payload.') 159 | } 160 | 161 | if (!github.context.payload.repository.name) { 162 | throw new Error('No name found in the repository.') 163 | } 164 | 165 | if ( 166 | !github.context.payload.repository.owner || 167 | (!github.context.payload.repository.owner.login && 168 | !github.context.payload.repository.owner.name) 169 | ) { 170 | throw new Error('No owner found in the repository.') 171 | } 172 | 173 | const commitMessages = await getCommitMessagesFromPullRequest( 174 | pullRequestOptions.accessToken, 175 | github.context.payload.repository.owner.name ?? 176 | github.context.payload.repository.owner.login, 177 | github.context.payload.repository.name, 178 | github.context.payload.pull_request.number 179 | ) 180 | 181 | for (message of commitMessages) { 182 | if (message) { 183 | messages.push(message) 184 | } 185 | } 186 | } 187 | 188 | break 189 | } 190 | case 'push': { 191 | if (!github.context.payload) { 192 | throw new Error('No payload found in the context.') 193 | } 194 | 195 | if ( 196 | !github.context.payload.commits || 197 | !github.context.payload.commits.length 198 | ) { 199 | core.debug(' - skipping commits') 200 | break 201 | } 202 | 203 | for (const i in github.context.payload.commits) { 204 | if (github.context.payload.commits[i].message) { 205 | messages.push(github.context.payload.commits[i].message) 206 | } 207 | } 208 | 209 | break 210 | } 211 | default: { 212 | throw new Error(`Event "${github.context.eventName}" is not supported.`) 213 | } 214 | } 215 | 216 | return messages 217 | } 218 | 219 | async function getCommitMessagesFromPullRequest( 220 | accessToken: string, 221 | repositoryOwner: string, 222 | repositoryName: string, 223 | pullRequestNumber: number 224 | ): Promise { 225 | core.debug('Get messages from pull request...') 226 | core.debug(` - accessToken: ${accessToken}`) 227 | core.debug(` - repositoryOwner: ${repositoryOwner}`) 228 | core.debug(` - repositoryName: ${repositoryName}`) 229 | core.debug(` - pullRequestNumber: ${pullRequestNumber}`) 230 | 231 | const query = ` 232 | query commitMessages( 233 | $repositoryOwner: String! 234 | $repositoryName: String! 235 | $pullRequestNumber: Int! 236 | $numberOfCommits: Int = 100 237 | ) { 238 | repository(owner: $repositoryOwner, name: $repositoryName) { 239 | pullRequest(number: $pullRequestNumber) { 240 | commits(last: $numberOfCommits) { 241 | edges { 242 | node { 243 | commit { 244 | message 245 | } 246 | } 247 | } 248 | } 249 | } 250 | } 251 | } 252 | ` 253 | const variables = { 254 | baseUrl: process.env['GITHUB_API_URL'] || 'https://api.github.com', 255 | repositoryOwner, 256 | repositoryName, 257 | pullRequestNumber, 258 | headers: { 259 | authorization: `token ${accessToken}` 260 | } 261 | } 262 | 263 | core.debug(` - query: ${query}`) 264 | core.debug(` - variables: ${JSON.stringify(variables, null, 2)}`) 265 | 266 | interface CommitEdgeItem { 267 | node: { 268 | commit: { 269 | message: string 270 | } 271 | } 272 | } 273 | 274 | interface RepositoryResponseData { 275 | repository: { 276 | pullRequest: { 277 | commits: { 278 | edges: [CommitEdgeItem] 279 | } 280 | } 281 | } 282 | } 283 | 284 | const response = await graphql(query, variables) 285 | const repository = response.repository 286 | 287 | core.debug(` - response: ${JSON.stringify(repository, null, 2)}`) 288 | 289 | let messages: string[] = [] 290 | 291 | if (repository.pullRequest) { 292 | messages = repository.pullRequest.commits.edges.map(function ( 293 | edge: CommitEdgeItem 294 | ): string { 295 | return edge.node.commit.message 296 | }) 297 | } 298 | 299 | return messages 300 | } 301 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the "GS Commit Message Checker" Action for Github. 3 | * 4 | * Copyright (C) 2019-2022 by Gilbertsoft LLC (gilbertsoft.org) 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 9 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 11 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 12 | * THE SOFTWARE. 13 | * 14 | * For the full license information, please read the LICENSE file that 15 | * was distributed with this source code. 16 | */ 17 | 18 | /** 19 | * Imports 20 | */ 21 | import * as core from '@actions/core' 22 | import * as inputHelper from './input-helper' 23 | import * as commitMessageChecker from './commit-message-checker' 24 | 25 | /** 26 | * Main function 27 | */ 28 | async function run(): Promise { 29 | try { 30 | const checkerArguments = await inputHelper.getInputs() 31 | if (checkerArguments.messages.length === 0) { 32 | core.info(`No commits found in the payload, skipping check.`) 33 | } else { 34 | await commitMessageChecker.checkCommitMessages(checkerArguments) 35 | } 36 | } catch (error) { 37 | if (error instanceof Error) { 38 | core.setFailed(error) 39 | } else { 40 | throw error // re-throw the error unchanged 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * Main entry point 47 | */ 48 | run() 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "outDir": "./lib", /* Redirect output structure to the directory. */ 6 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | "baseUrl": "./" 11 | }, 12 | "exclude": ["node_modules", "**/*.test.ts"] 13 | } 14 | --------------------------------------------------------------------------------