├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml ├── release.yml └── workflows │ └── validate-action-typings.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── action-types.yml ├── action.yml ├── codegen.yml ├── dist ├── 348.index.js ├── index.js └── setup-credentials.sh ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── src ├── generated │ └── graphql.ts ├── main.ts ├── mutations │ ├── add-labels.graphql │ ├── add-reviewers.graphql │ ├── create-pr.graphql │ ├── delete-branch.graphql │ └── update-pr.graphql ├── parse-browserslist-output.ts ├── queries │ ├── browserslist-update-branch.graphql │ ├── labels.graphql │ ├── organisation.graphql │ └── user.graphql └── setup-credentials.sh └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 99 8 | labels: 9 | - dependabot 10 | - package-ecosystem: github-actions 11 | directory: '/' 12 | schedule: 13 | interval: 'weekly' 14 | labels: 15 | - dependabot 16 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: 🚨 Breaking changes 4 | labels: 5 | - backwards-incompatible 6 | - breaking 7 | - title: ✨ New features and enhancements 8 | labels: 9 | - enhancement 10 | - title: 🐞 Fixed bugs 11 | labels: 12 | - bug 13 | - title: ⚠️ Deprecated 14 | labels: 15 | - deprecated 16 | - title: ⛔ Removed 17 | labels: 18 | - removed 19 | - title: 📚 Documentation 20 | labels: 21 | - documentation 22 | - title: 🏗 Chores 23 | labels: 24 | - chore 25 | - dependency 26 | - dependencies 27 | - security 28 | - dependabot 29 | - title: Other changes 30 | labels: 31 | - '*' 32 | -------------------------------------------------------------------------------- /.github/workflows/validate-action-typings.yml: -------------------------------------------------------------------------------- 1 | name: Validate action typings 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | validate-typings: 11 | runs-on: 'ubuntu-latest' 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: krzema12/github-actions-typing@v2 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | .dccache 107 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | # generated files 5 | src/generated/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "semi": true, 6 | "endOfLine": "lf" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 camptocamp.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 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 | # browserslist-update-action 2 | 3 | A GitHub Action that runs `update-browserslist-db@latest` on a repository and proposes a pull request to merge updates. Since v2.5, also supports other package managers (yarn, pnpm, bun, deno). 4 | 5 | ## v2 vs v1 6 | 7 | `v1` uses command `browserslist@latest --update-db` which is now deprecated and to be removed in a future major `browserslist` release. It should be dropped in favor of `v2` which uses new command `update-browserslist-db@latest`. 8 | 9 | :warning: v2 requires at least Node 15 (`npm` 6). Stick to `v1` otherwise. 10 | 11 | ## Usage 12 | 13 | ```yaml 14 | name: Update Browserslist database 15 | 16 | on: 17 | schedule: 18 | - cron: '0 2 1,15 * *' 19 | 20 | permissions: 21 | contents: write 22 | pull-requests: write 23 | 24 | jobs: 25 | update-browserslist-database: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 32 | - name: Configure git 33 | run: | 34 | # Setup for commiting using built-in token. See https://github.com/actions/checkout#push-a-commit-using-the-built-in-token 35 | git config user.name "github-actions[bot]" 36 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 37 | - name: Update Browserslist database and create PR if applies 38 | uses: c2corg/browserslist-update-action@v2 39 | with: 40 | github_token: ${{ secrets.GITHUB_TOKEN }} 41 | branch: browserslist-update 42 | base_branch: main 43 | commit_message: 'build: update Browserslist db' 44 | title: 'build: update Browserslist db' 45 | body: Auto-generated by [browserslist-update-action](https://github.com/c2corg/browserslist-update-action/) 46 | labels: 'chores, github action' 47 | reviewers: 'user1,user2,user3' 48 | teams: 'team1' 49 | ``` 50 | 51 | > [!IMPORTANT] 52 | > This action can only create a pull request if the _Allow GitHub Actions to create and approve pull requests_ option is enabled in _Settings > Actions > General_. 53 | 54 | ## Action inputs 55 | 56 | Inputs with defaults are **optional**. 57 | 58 | | Name | Description | Default | 59 | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | 60 | | github_token | `GITHUB_TOKEN` (`contents: write`, `pull-requests: write`) or a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). | `GITHUB_TOKEN` | 61 | | branch | The pull request branch name. | `browserslist-update` | 62 | | base_branch | The target branch into which the pull request will be merged. | `master` | 63 | | directory | For monorepos, the directory to switch to when running action | `.` | 64 | | commit_message | The message to use when committing changes. | `Update caniuse database` | 65 | | title | The title of the pull request. | `📈 Update caniuse database` | 66 | | body | The body of the pull request. | `Caniuse database has been updated. Review changes, merge this PR and have a 🍺.` | 67 | | labels | A comma separated list of labels to apply to the pull request. | (no label) | 68 | | reviewers | A comma separated list of users (either login or id) to be added to the the pull request reviewers list. | (no users) | 69 | | teams | A comma separated list of teams (either name or id) to be added to the the pull request reviewers list. | (no teams) | 70 | 71 | One will usually run this action on a cron basis (say, every day or week) 72 | 73 | ## Action outputs 74 | 75 | | Name | Description | 76 | | --------- | -------------------------------------------------------------------------------------------------- | 77 | | has_pr | `true` if changes were found and a pull request was created or updated, `false` otherwise. | 78 | | pr_number | The number of the pull requested created or updated, if applies. | 79 | | pr_status | Can be either `created` or `updated` depending on whether the pull request was created or updated. | 80 | 81 | ## Contributing 82 | 83 | ### Edit / add GraphQL queries and mutations 84 | 85 | `src/generated` folder contains generated type definitions based on queries. Run `npm run graphql` to update. 86 | 87 | ### Release a version 88 | 89 | ```sh 90 | npm run lint 91 | npm run build 92 | npm run pack 93 | ``` 94 | 95 | Or shortly 96 | 97 | ```sh 98 | npm run all 99 | ``` 100 | 101 | Then bump version number in `package.json` and `package-lock.json`. Push commits. 102 | 103 | Keep a major version tag synchronized with updates, e.g. if you publish version `v2.0.3`, then a `v2` branch should be positioned at the same location. 104 | -------------------------------------------------------------------------------- /action-types.yml: -------------------------------------------------------------------------------- 1 | inputs: 2 | github_token: 3 | type: string 4 | branch: 5 | type: string 6 | base_branch: 7 | type: string 8 | directory: 9 | type: string 10 | commit_message: 11 | type: string 12 | title: 13 | type: string 14 | body: 15 | type: string 16 | labels: 17 | type: list 18 | separator: ',' 19 | list-item: 20 | type: string 21 | reviewers: 22 | type: list 23 | separator: ',' 24 | list-item: 25 | type: string 26 | teams: 27 | type: list 28 | separator: ',' 29 | list-item: 30 | type: string 31 | outputs: 32 | has_pr: 33 | type: boolean 34 | pr_number: 35 | type: integer 36 | pr_status: 37 | type: enum 38 | allowed-values: 39 | - created 40 | - updated 41 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: browserslist-update-action 2 | description: Runs `update-browserslist-db@latest` on a repository and proposes a pull request to merge updates. 3 | author: Camptocamp Association 4 | branding: 5 | icon: bar-chart-2 6 | color: green 7 | inputs: 8 | github_token: 9 | description: GitHub secret 10 | required: true 11 | branch: 12 | description: The pull request branch name 13 | required: false 14 | default: browserslist-update 15 | base_branch: 16 | description: The target branch into which the pull request will be merged 17 | directory: 18 | description: For monorepos, directory to switch to 19 | required: false 20 | default: . 21 | commit_message: 22 | description: The message to use when committing changes 23 | required: false 24 | default: Update caniuse database 25 | title: 26 | description: The title of the pull request 27 | required: false 28 | default: 📈 Update caniuse database 29 | body: 30 | description: The body of the pull request 31 | required: false 32 | default: Caniuse database has been updated. Review changes, merge this PR and have a 🍺. 33 | labels: 34 | description: Labels to associate to the pull request 35 | required: false 36 | reviewers: 37 | description: Users to associate to the pull request reviewers list 38 | required: false 39 | teams: 40 | description: Teams to associate to the pull request reviewers list 41 | required: false 42 | outputs: 43 | has_pr: 44 | description: A boolean set to true when changes were found and a pull request was created or updated. 45 | pr_number: 46 | description: The pull request number, if applies. 47 | pr_status: 48 | description: Whether the pull request was created or updated, if applies. 49 | runs: 50 | using: 'node20' 51 | main: 'dist/index.js' 52 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 'node_modules/@octokit/graphql-schema/schema.json' 3 | documents: 4 | - src/queries/*.graphql 5 | - src/mutations/*.graphql 6 | generates: 7 | src/generated/graphql.ts: 8 | plugins: 9 | - 'typescript' 10 | - 'typescript-resolvers' 11 | - 'typescript-document-nodes' 12 | - 'typescript-operations' 13 | -------------------------------------------------------------------------------- /dist/348.index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.id = 348; 3 | exports.ids = [348]; 4 | exports.modules = { 5 | 6 | /***/ 348: 7 | /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { 8 | 9 | // ESM COMPAT FLAG 10 | __webpack_require__.r(__webpack_exports__); 11 | 12 | // EXPORTS 13 | __webpack_require__.d(__webpack_exports__, { 14 | "default": () => (/* binding */ stripAnsi) 15 | }); 16 | 17 | ;// CONCATENATED MODULE: ./node_modules/ansi-regex/index.js 18 | function ansiRegex({onlyFirst = false} = {}) { 19 | const pattern = [ 20 | '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', 21 | '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))' 22 | ].join('|'); 23 | 24 | return new RegExp(pattern, onlyFirst ? undefined : 'g'); 25 | } 26 | 27 | ;// CONCATENATED MODULE: ./node_modules/strip-ansi/index.js 28 | 29 | 30 | const regex = ansiRegex(); 31 | 32 | function stripAnsi(string) { 33 | if (typeof string !== 'string') { 34 | throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``); 35 | } 36 | 37 | // Even though the regex is global, we don't need to reset the `.lastIndex` 38 | // because unlike `.exec()` and `.test()`, `.replace()` does it automatically 39 | // and doing it manually has a performance penalty. 40 | return string.replace(regex, ''); 41 | } 42 | 43 | 44 | /***/ }) 45 | 46 | }; 47 | ; -------------------------------------------------------------------------------- /dist/setup-credentials.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | 4 | # Set up .netrc file with GitHub credentials 5 | cat <<-EOF >$HOME/.netrc 6 | machine github.com 7 | login $GITHUB_ACTOR 8 | password $INPUT_GITHUB_TOKEN 9 | machine api.github.com 10 | login $GITHUB_ACTOR 11 | password $INPUT_GITHUB_TOKEN 12 | EOF 13 | chmod 600 $HOME/.netrc 14 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | 6 | export default tseslint.config( 7 | { ignores: ['dist/**', 'lib/**', 'node_modules/**', 'src/generated/graphql.ts', '*.sh', '*.graphql'] }, 8 | eslint.configs.recommended, 9 | tseslint.configs.recommended, 10 | { 11 | rules: { 12 | '@typescript-eslint/explicit-function-return-type': 'error', 13 | '@typescript-eslint/camelcase': 'off', 14 | }, 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserslist-update-action", 3 | "version": "2.5.0", 4 | "private": "true", 5 | "description": "A Github Action to run `npx browserslist@latest --update-db` on a repository and propose a pull request to merge updates", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "lint": "prettier --check \"**/*.{ts,js,json,css,scss,less,md,html}\" && eslint . --report-unused-disable-directives", 10 | "graphql": "graphql-codegen --config codegen.yml", 11 | "pack": "ncc build && cp src/*.sh dist/", 12 | "all": "npm run graphql && npm run lint && npm run build && npm run pack", 13 | "prepare": "husky", 14 | "preversion": "npm run all && git add -A src/generated dist" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/c2corg/browserslist-update-action.git" 19 | }, 20 | "keywords": [ 21 | "actions", 22 | "node" 23 | ], 24 | "author": "Camptocamp Association", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/c2corg/browserslist-update-action/issues" 28 | }, 29 | "homepage": "https://github.com/c2corg/browserslist-update-action#readme", 30 | "dependencies": { 31 | "@actions/core": "1.11.1", 32 | "@actions/exec": "1.1.1", 33 | "@actions/github": "6.0.0", 34 | "@octokit/core": "6.1.5", 35 | "@octokit/graphql": "8.2.2", 36 | "@octokit/plugin-paginate-graphql": "5.2.4", 37 | "package-manager-detector": "1.1.0", 38 | "strip-ansi": "7.1.0" 39 | }, 40 | "devDependencies": { 41 | "@eslint/js": "9.24.0", 42 | "@graphql-codegen/cli": "5.0.5", 43 | "@graphql-codegen/typescript": "4.1.6", 44 | "@graphql-codegen/typescript-document-nodes": "4.0.16", 45 | "@graphql-codegen/typescript-operations": "4.6.0", 46 | "@graphql-codegen/typescript-resolvers": "4.5.0", 47 | "@octokit/graphql-schema": "15.26.0", 48 | "@tsconfig/node20": "20.1.5", 49 | "@tsconfig/strictest": "2.0.5", 50 | "@types/gettext-parser": "8.0.0", 51 | "@types/node": "22.14.0", 52 | "@vercel/ncc": "0.38.3", 53 | "eslint": "9.24.0", 54 | "graphql": "16.10.0", 55 | "husky": "9.1.7", 56 | "lint-staged": "15.5.0", 57 | "prettier": "3.5.3", 58 | "typescript": "5.8.3", 59 | "typescript-eslint": "8.29.1" 60 | }, 61 | "lint-staged": { 62 | "*.(ts|js|json|css|scss|md|html)": [ 63 | "prettier --write --ignore-unknown --list-different" 64 | ], 65 | "*.ts": [ 66 | "eslint --fix --report-unused-disable-directives" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { exec } from '@actions/exec'; 3 | import * as github from '@actions/github'; 4 | import { GitHub, getOctokitOptions } from '@actions/github/lib/utils'; 5 | import { paginateGraphql } from '@octokit/plugin-paginate-graphql'; 6 | import { print } from 'graphql/language/printer'; 7 | import { detect, resolveCommand } from 'package-manager-detector'; 8 | import { join as path } from 'path'; 9 | import { chdir, cwd } from 'process'; 10 | import { 11 | AddLabels, 12 | AddReviewers, 13 | BrowserslistUpdateBranch, 14 | BrowserslistUpdateBranchQueryVariables, 15 | CreatePr, 16 | DeleteBranch, 17 | Labels, 18 | Organization, 19 | OrganizationQuery, 20 | OrganizationQueryVariables, 21 | UpdatePullRequest, 22 | User, 23 | UserQuery, 24 | UserQueryVariables, 25 | type AddLabelsMutation, 26 | type AddLabelsMutationVariables, 27 | type AddReviewersMutation, 28 | type AddReviewersMutationVariables, 29 | type BrowserslistUpdateBranchQuery, 30 | type CreatePrMutation, 31 | type CreatePrMutationVariables, 32 | type DeleteBranchMutation, 33 | type DeleteBranchMutationVariables, 34 | type LabelsQuery, 35 | type LabelsQueryVariables, 36 | type UpdatePullRequestMutation, 37 | type UpdatePullRequestMutationVariables, 38 | } from './generated/graphql'; 39 | import { parse } from './parse-browserslist-output'; 40 | 41 | const githubToken = core.getInput('github_token', { required: true }); 42 | const repositoryOwner = github.context.repo.owner; 43 | const repositoryName = github.context.repo.repo; 44 | const branch = core.getInput('branch') || 'browserslist-update'; 45 | const baseBranch = core.getInput('base_branch') || 'master'; 46 | const labels = (core.getInput('labels') || '') 47 | .split(',') 48 | .map((label) => label.trim()) 49 | .filter((label) => !!label); 50 | 51 | const MyOctokit = GitHub.plugin(paginateGraphql); 52 | const octokit = new MyOctokit(getOctokitOptions(githubToken)); 53 | 54 | async function run(): Promise { 55 | try { 56 | core.info('Check if there is a branch and a matching PR already existing for caniuse db update'); 57 | const browserslistUpdateBranchQueryData: BrowserslistUpdateBranchQueryVariables = { 58 | owner: repositoryOwner, 59 | name: repositoryName, 60 | branch, 61 | }; 62 | const browserslistUpdateBranchQuery = await octokit.graphql({ 63 | query: print(BrowserslistUpdateBranch), 64 | ...browserslistUpdateBranchQueryData, 65 | }); 66 | 67 | let browserslistUpdateBranchExists = browserslistUpdateBranchQuery.repository?.refs?.totalCount || false; 68 | let browserslistUpdatePR: string | undefined = undefined; 69 | if (browserslistUpdateBranchExists) { 70 | const pullRequests = browserslistUpdateBranchQuery.repository?.refs?.edges?.[0]?.node?.associatedPullRequests; 71 | if (pullRequests?.totalCount === 1) { 72 | browserslistUpdatePR = pullRequests.edges?.[0]?.node?.id; 73 | } 74 | } 75 | if (browserslistUpdateBranchExists && !browserslistUpdatePR) { 76 | // delete branch first, it should have been done anyway when previous PR was merged 77 | core.info(`Branch ${branch} already exists but no PR associated, delete it first`); 78 | const mutationData: DeleteBranchMutationVariables = { 79 | input: { 80 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 81 | refId: browserslistUpdateBranchQuery.repository?.refs?.edges?.[0]?.node?.id!, 82 | }, 83 | }; 84 | octokit.graphql({ query: print(DeleteBranch), ...mutationData }); 85 | browserslistUpdateBranchExists = !browserslistUpdateBranchExists; 86 | } 87 | 88 | // keep track of current branch 89 | let currentBranch = ''; 90 | await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { 91 | listeners: { 92 | stdout: (data: Buffer): void => { 93 | currentBranch += data.toString().trim(); 94 | }, 95 | }, 96 | }); 97 | 98 | if (browserslistUpdateBranchExists) { 99 | core.info(`Checkout branch ${branch}`); 100 | await exec('git', ['fetch']); 101 | await exec('git', ['checkout', branch]); 102 | await exec('git', ['rebase', `origin/${baseBranch}`]); 103 | } else { 104 | core.info(`Create new branch ${branch}`); 105 | await exec('git', ['checkout', '-b', branch]); 106 | } 107 | 108 | // Run npx browserslist update 109 | const subDir = (core.getInput('directory') || '.').trim(); 110 | const currentDir = cwd(); 111 | if (subDir !== '.') { 112 | core.info(`Switching to dir ${subDir} to run update db command`); 113 | chdir(subDir); 114 | } 115 | let browserslistOutput = ''; 116 | const pkgMgr = await detect(); 117 | if (!pkgMgr) { 118 | core.setFailed('Could not detect package manager'); 119 | return; 120 | } 121 | const { command, args } = resolveCommand(pkgMgr.agent, 'execute', ['update-browserslist-db@latest'])!; 122 | await exec(command, args, { 123 | listeners: { 124 | stdout: (data: Buffer) => { 125 | browserslistOutput += data.toString(); 126 | }, 127 | }, 128 | }); 129 | if (subDir !== '.') { 130 | chdir(currentDir); 131 | } 132 | 133 | core.info('Check whether new files bring modifications to the current branch'); 134 | let gitStatus = ''; 135 | await exec('git', ['status', '-s'], { 136 | listeners: { 137 | stdout: (data: Buffer): void => { 138 | gitStatus += data.toString().trim(); 139 | }, 140 | }, 141 | }); 142 | if (!gitStatus.trim()) { 143 | core.setOutput('has_pr', false); 144 | core.info('No changes. Exiting'); 145 | return; 146 | } 147 | 148 | core.setOutput('has_pr', true); 149 | 150 | core.info('Add files and commit on base branch'); 151 | await exec('git', ['add', '.']); 152 | await exec('git', ['commit', '-m', core.getInput('commit_message') || 'Update caniuse database']); 153 | 154 | // setup credentials 155 | await exec('bash', [path(__dirname, 'setup-credentials.sh')]); 156 | 157 | core.info('Push branch to origin'); 158 | if (browserslistUpdateBranchExists) { 159 | await exec('git', ['push', '--force']); 160 | } else { 161 | await exec('git', ['push', '--set-upstream', 'origin', branch]); 162 | } 163 | 164 | let prNumber: number; 165 | let prId: string; 166 | 167 | // create PR if not exists 168 | if (!browserslistUpdatePR) { 169 | core.info(`Creating new PR for branch ${branch}`); 170 | const title = core.getInput('title') || '📈 Update caniuse database'; 171 | const body = core.getInput('body') || (await prBody(browserslistOutput)); 172 | const mutationData: CreatePrMutationVariables = { 173 | input: { 174 | title, 175 | body, 176 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 177 | repositoryId: browserslistUpdateBranchQuery.repository?.id!, 178 | baseRefName: baseBranch, 179 | headRefName: branch, 180 | }, 181 | }; 182 | const response = await octokit.graphql({ query: print(CreatePr), ...mutationData }); 183 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 184 | prNumber = response.createPullRequest?.pullRequest?.number!; 185 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 186 | prId = response.createPullRequest?.pullRequest?.id!; 187 | core.setOutput('pr_status', 'created'); 188 | } else { 189 | core.info('PR already exists, updating'); 190 | const body = core.getInput('body') || (await prBody(browserslistOutput)); 191 | const mutationData: UpdatePullRequestMutationVariables = { 192 | input: { 193 | pullRequestId: browserslistUpdatePR, 194 | body, 195 | }, 196 | }; 197 | const response = await octokit.graphql({ 198 | query: print(UpdatePullRequest), 199 | ...mutationData, 200 | }); 201 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 202 | prNumber = response.updatePullRequest?.pullRequest?.number!; 203 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 204 | prId = response.updatePullRequest?.pullRequest?.id!; 205 | 206 | core.setOutput('pr_status', 'updated'); 207 | } 208 | core.setOutput('pr_number', prNumber); 209 | 210 | // apply labels (if matching label found, do not attempt to create missing label) 211 | const labelsQueryData: LabelsQueryVariables = { 212 | owner: repositoryOwner, 213 | name: repositoryName, 214 | }; 215 | const labelIds = 216 | (await octokit.graphql.paginate(print(Labels), labelsQueryData)).repository?.labels?.nodes 217 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 218 | ?.filter((node) => labels.includes(node?.name!)) 219 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 220 | .map((node) => node?.id!) ?? []; 221 | if (labelIds.length) { 222 | const addLabelsMutationData: AddLabelsMutationVariables = { 223 | input: { 224 | labelableId: prId, 225 | labelIds, 226 | }, 227 | }; 228 | await octokit.graphql({ query: print(AddLabels), ...addLabelsMutationData }); 229 | } 230 | 231 | let reviewers = (core.getInput('reviewers') || '') 232 | .split(',') 233 | .map((reviewer) => reviewer.trim()) 234 | .filter(Boolean); 235 | reviewers = await Promise.all( 236 | reviewers.map(async (reviewer) => { 237 | // try to replace reviewer login by its corresponding ID. Otherwise, consider we already deal with the ID 238 | const userQueryData: UserQueryVariables = { login: reviewer }; 239 | return octokit.graphql({ query: print(User), ...userQueryData }).then( 240 | (response) => response?.user?.id ?? reviewer, 241 | () => reviewer, 242 | ); 243 | }), 244 | ); 245 | let teamReviewers = (core.getInput('teams') || '') 246 | .split(',') 247 | .map((team) => team.trim()) 248 | .filter((team) => !!team); 249 | 250 | if (teamReviewers.length) { 251 | try { 252 | const organizationQueryData: OrganizationQueryVariables = { login: repositoryOwner }; 253 | const teams = new Map( 254 | ( 255 | (await octokit.graphql.paginate(print(Organization), organizationQueryData)) 256 | ?.organization?.teams?.nodes ?? [] 257 | ).map((team) => [team?.name, team?.id]), 258 | ); 259 | teamReviewers = teamReviewers.map((team) => teams.get(team) ?? team); 260 | } catch { 261 | core.warning('Unable to retrieve organization info. Is this repository owned by an organization?'); 262 | } 263 | } 264 | 265 | if (reviewers.length || teamReviewers.length) { 266 | core.info('Adding reviewers to the PR'); 267 | const addReviewersMutationData: AddReviewersMutationVariables = { 268 | input: { 269 | pullRequestId: prId, 270 | union: true, 271 | userIds: reviewers, 272 | teamIds: teamReviewers, 273 | }, 274 | }; 275 | await octokit.graphql({ 276 | query: print(AddReviewers), 277 | ...addReviewersMutationData, 278 | }); 279 | } 280 | 281 | // go back to previous branch 282 | await exec('git', ['checkout', currentBranch]); 283 | } catch (error) { 284 | if (error instanceof Error) { 285 | core.setFailed(error.message); 286 | } else { 287 | core.setFailed('Error'); 288 | } 289 | } 290 | } 291 | 292 | async function prBody(browserslistOutput: string): Promise { 293 | const info = await parse(browserslistOutput); 294 | const msg = ['Caniuse database has been updated. Review changes, merge this PR and have a 🍺.']; 295 | if (info.installedVersion) { 296 | msg.push(`Installed version: ${info.installedVersion}`); 297 | } 298 | if (info.latestVersion) { 299 | msg.push(`Latest version: ${info.latestVersion}`); 300 | } 301 | if (info.browsersAdded.length || info.browsersRemoved.length) { 302 | msg.push('Target browsers changes: '); 303 | msg.push('\n'); 304 | msg.push('```diff'); 305 | info.browsersRemoved.forEach((value) => msg.push('- ' + value)); 306 | info.browsersAdded.forEach((value) => msg.push('+ ' + value)); 307 | msg.push('```'); 308 | } 309 | return msg.join('\n'); 310 | } 311 | 312 | run(); 313 | -------------------------------------------------------------------------------- /src/mutations/add-labels.graphql: -------------------------------------------------------------------------------- 1 | mutation AddLabels($input: AddLabelsToLabelableInput!) { 2 | addLabelsToLabelable(input: $input) { 3 | clientMutationId 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/mutations/add-reviewers.graphql: -------------------------------------------------------------------------------- 1 | mutation AddReviewers($input: RequestReviewsInput!) { 2 | requestReviews(input: $input) { 3 | clientMutationId 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/mutations/create-pr.graphql: -------------------------------------------------------------------------------- 1 | mutation CreatePR($input: CreatePullRequestInput!) { 2 | createPullRequest(input: $input) { 3 | pullRequest { 4 | id 5 | number 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/mutations/delete-branch.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteBranch($input: DeleteRefInput!) { 2 | deleteRef(input: $input) { 3 | clientMutationId 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/mutations/update-pr.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdatePullRequest($input: UpdatePullRequestInput!) { 2 | updatePullRequest(input: $input) { 3 | pullRequest { 4 | id 5 | number 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/parse-browserslist-output.ts: -------------------------------------------------------------------------------- 1 | export type BrowserslistInfo = { 2 | latestVersion: string; 3 | installedVersion: string; 4 | browsersAdded: string[]; 5 | browsersRemoved: string[]; 6 | }; 7 | 8 | export const parse = async (output: string): Promise => { 9 | let latestVersion = ''; 10 | let installedVersion = ''; 11 | const browsersAdded: string[] = []; 12 | const browsersRemoved: string[] = []; 13 | 14 | let isListingChanges = false; 15 | const stripAnsi = (await import('strip-ansi')).default; 16 | stripAnsi(output) 17 | .split('\n') 18 | .forEach((line) => { 19 | let match: string[] | null; 20 | if (isListingChanges) { 21 | match = line.match('([-+])\\s(.*)'); 22 | if (match?.[1] === '+') { 23 | browsersAdded.push(match[2] as string); 24 | } else if (match?.[1] === '-') { 25 | browsersRemoved.push(match[2] as string); 26 | } 27 | return; 28 | } 29 | match = line.match('Latest version:\\s+(.*)'); 30 | if (match?.[1]) { 31 | latestVersion = match?.[1]; 32 | return; 33 | } 34 | match = line.match('Installed version:\\s+(.*)'); 35 | if (match?.[1]) { 36 | installedVersion = match?.[1]; 37 | return; 38 | } 39 | if (line.match('Target browser changes:')) { 40 | isListingChanges = true; 41 | } 42 | }); 43 | return { 44 | installedVersion, 45 | latestVersion, 46 | browsersAdded, 47 | browsersRemoved, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/queries/browserslist-update-branch.graphql: -------------------------------------------------------------------------------- 1 | query BrowserslistUpdateBranch($owner: String!, $name: String!, $branch: String!) { 2 | repository(owner: $owner, name: $name) { 3 | id 4 | refs(refPrefix: "refs/heads/", query: $branch, first: 1) { 5 | totalCount 6 | edges { 7 | node { 8 | id 9 | associatedPullRequests(first: 1, states: [OPEN]) { 10 | totalCount 11 | edges { 12 | node { 13 | id 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/queries/labels.graphql: -------------------------------------------------------------------------------- 1 | query Labels($owner: String!, $name: String!, $cursor: String) { 2 | repository(owner: $owner, name: $name) { 3 | id 4 | labels(first: 10, after: $cursor) { 5 | totalCount 6 | nodes { 7 | id 8 | name 9 | } 10 | pageInfo { 11 | hasNextPage 12 | endCursor 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/queries/organisation.graphql: -------------------------------------------------------------------------------- 1 | query Organization($login: String!, $cursor: String) { 2 | organization(login: $login) { 3 | teams(first: 10, after: $cursor) { 4 | nodes { 5 | id 6 | name 7 | } 8 | pageInfo { 9 | hasNextPage 10 | endCursor 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/queries/user.graphql: -------------------------------------------------------------------------------- 1 | query User($login: String!) { 2 | user(login: $login) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/setup-credentials.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | 4 | # Set up .netrc file with GitHub credentials 5 | cat <<-EOF >$HOME/.netrc 6 | machine github.com 7 | login $GITHUB_ACTOR 8 | password $INPUT_GITHUB_TOKEN 9 | machine api.github.com 10 | login $GITHUB_ACTOR 11 | password $INPUT_GITHUB_TOKEN 12 | EOF 13 | chmod 600 $HOME/.netrc 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/node20/tsconfig.json"], 3 | "compilerOptions": { 4 | "module": "NodeNext", 5 | "outDir": "./lib", 6 | "rootDir": "./src", 7 | "importsNotUsedAsValues": "remove" 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["node_modules", "**/*.test.ts"] 11 | } 12 | --------------------------------------------------------------------------------