├── .editorconfig ├── .envrc ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── default_issue_template.md ├── dependabot.yaml └── workflows │ ├── issue-notifier.yml │ └── main.yml ├── .gitignore ├── LICENSE ├── README.adoc ├── action.yml ├── assets └── niv-update-action-changelog.png ├── devenv.lock ├── devenv.nix ├── devenv.yaml ├── main.js └── niv-updater /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | # options for shfmt(1) 11 | binary_next_line = true 12 | switch_case_indent = true 13 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | watch_file devenv.nix 2 | watch_file devenv.yaml 3 | watch_file devenv.lock 4 | eval "$(devenv print-dev-env)" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/default_issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Default Issue Template 3 | about: The default issue template that automatically assigns the owner. 4 | title: '' 5 | labels: "@project/notify-owners" 6 | assignees: knl 7 | 8 | --- 9 | 10 | 13 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/issue-notifier.yml: -------------------------------------------------------------------------------- 1 | name: Notify owners on new issues 2 | on: 3 | issues: 4 | types: [edited, labeled] 5 | 6 | jobs: 7 | sendEmail: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | - name: issue-notifier 13 | uses: timheuer/issue-notifier@v1.0.4 14 | env: 15 | SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }} 16 | with: 17 | fromMailAddress: 'nikola+sendgrid@knezevic.ch' 18 | toMailAddress: 'github@nikola.knezevic.ch' 19 | subject: 'A new issue was labeled/created in niv-updater-action' 20 | labelsToMonitor: '@project/notify-owners' 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.adoc' 9 | - 'LICENSE' 10 | - '.gitignore' 11 | pull_request: 12 | branches: 13 | - main 14 | paths-ignore: 15 | - 'README.adoc' 16 | - 'LICENSE' 17 | - '.gitignore' 18 | 19 | jobs: 20 | lint: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout source code 24 | uses: actions/checkout@v4 25 | - name: Install dependencies 26 | run: | 27 | # Get Nix 28 | sudo mkdir -p /etc/nix 29 | # Workaround segfault: https://github.com/NixOS/nix/issues/2733 30 | sudo sh -c 'echo "http2 = false" >> /etc/nix/nix.conf' 31 | sh <(curl -sSL https://nixos.org/nix/install) --no-daemon 32 | 33 | echo "/nix/var/nix/profiles/per-user/runner/profile/bin" >> $GITHUB_PATH 34 | echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH 35 | echo "${HOME}/.nix-profile/bin" >> $GITHUB_PATH 36 | 37 | source "${HOME}/.nix-profile/etc/profile.d/nix.sh" 38 | # Install dependencies 39 | nix-env -iA nixpkgs.shfmt nixpkgs.shellcheck 40 | - name: Run shfmt 41 | run: | 42 | shfmt -d -i 4 -ci -bn -s niv-updater 43 | - name: Run shellcheck 44 | run: | 45 | shellcheck -s bash niv-updater 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Devenv 3 | .devenv* 4 | devenv.local.nix 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Nikola Knežević 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = niv-updater: Automated dependency updates with niv 2 | 3 | image:https://github.com/knl/niv-updater-action/actions/workflows/main.yml/badge.svg[CI] 4 | image:https://img.shields.io/github/v/release/knl/niv-updater-action[GitHub release (latest by date)] 5 | 6 | This action will open a pull request to `master` branch (or otherwise specified 7 | branch) whenever https://github.com/nmattia/niv[niv] detects updates to 8 | `nix/sources.json` in your repository, for each dependency separately. Each PR 9 | will contain a beautiful Changelog of all the changes in the update, like this: 10 | 11 | image:./assets/niv-update-action-changelog.png[title="Changelog generated by niv-updater-action] 12 | 13 | The best way to use `niv-updater-action` is to set up a scheduled workflow. This 14 | way, whenever there are new updates, you will get a PR that you can just 15 | approve and avoid a lot of manual work. 16 | 17 | == Example 18 | 19 | Here is an minimal example of what to put in your 20 | `+.github/workflows/niv-updates.yml+` file to trigger the action. 21 | 22 | [source,yaml] 23 | ---- 24 | name: Automated niv-managed dependency updates 25 | on: 26 | schedule: 27 | # * is a special character in YAML so you have to quote this string 28 | # run this every day at 4:00am 29 | - cron: '0 4 * * *' 30 | jobs: 31 | niv-updater: 32 | name: 'Create PRs for niv-managed dependencies' 33 | runs-on: ubuntu-latest 34 | steps: 35 | # notice there is no checkout step 36 | - name: niv-updater-action 37 | uses: knl/niv-updater-action@v15 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | ---- 41 | 42 | == Configuration 43 | 44 | === Inputs 45 | 46 | `niv-updater-action` is configured using the following inputs: 47 | 48 | * `pull_request_base`: (Optional) The name of the branch to issue the pull request 49 | against. In addition, the name of the branch from which `nix/sources.json` is 50 | taken from as the base. Defaults to an empty string which means taking as the 51 | base the default branch for the repository. 52 | * `sources_file`: (Optional) The path in the repo to the `sources.json` file. 53 | This value will be passed to niv via `--sources-file` option. Defaults to 54 | `nix/sources.json`. 55 | * `niv_version`: (Optional) The niv version to be used. Defaults to `master`, 56 | meaning `niv-updater-action` will take the latest niv for each run. You may want 57 | to fix a particular version and avoid future breaks to your workflow. If you're 58 | using a self-hosted runner, set this to `*from-nixpkgs*`. 59 | * `branch_prefix`: (Optional) The prefix used for update branches, created by 60 | this action. The action does not sanitize the branch name. For a description 61 | of what a valid branch name is, please consult: 62 | https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html. 63 | Defaults to `update/`. 64 | * `keep_updating`: (Optional) If PR already exists, keep it updated with new 65 | changes. The branch will be force updated, as this process keeps a single 66 | commit on a branch. Defaults to `false` to maintain the old behaviour. 67 | * `skip_versioned_revisions`: (Optional, a boolean) If `true`, will cause the 68 | action to skip updating any dependency that has a version in their revision. 69 | This is due to the way niv currently works, where it will always update to the 70 | latest HEAD of a branch. Thus, if one have a dependency where, for example, 71 | `rev=v1.0.0`, niv would normally update it to the latest head of the branch, 72 | making `rev` holding the appropriate SHA. This is something one would not 73 | normally expect. Thus, this option exists until niv fixes the behaviour. 74 | Defaults to `true`. 75 | * `skip_ssh_repos`: (Optional, a boolean) If `true`, will cause the action to 76 | skip updating any dependency that is hosted by a repo accessible via ssh. 77 | Defaults to `false`. 78 | * `whitelist`: (Optional) A list of dependencies, comma separated, that will be 79 | checked for updates. This list will be checked *before* the blacklist. 80 | Defaults to an empty string, which is a _special case_ for looking into all 81 | dependencies tracked by `niv`. 82 | * `blacklist`: (Optional) A list of dependencies, comma separated, to skip from 83 | updating. This list will be checked *after* the whitelist. Defaults to an 84 | empty string, which means all dependencies will be checked for updates. 85 | * `labels`: (Optional) A list of labels, **newline** separated, to apply to all 86 | created PRs. Defaults to an empty string, meaning no labels will be applied. 87 | The list has to be newline separated (use YAML's `|` block), as GitHub allows 88 | various characters in the label's name, except the newline. 89 | * `show_merges`: (Optional, a boolean) If `true`, the changelog will contain 90 | merge commits listed. Otherwise, they will be skipped (however, the commits 91 | from the PRs/branches will shown). Defaults to `false`. 92 | * `message_prefix`: (Optional) The text that will be put in front of the 93 | generated changelog. Defaults to empty. 94 | * `message_suffix`: (Optional) The text that will be put in after the generated 95 | changelog. Defaults to empty. 96 | * `title_prefix`: (Optional) The text that will be put in front of the 97 | generated commit title. Defaults to empty. 98 | * `github_changelog_no_backreferences`: (Optional, a boolean) If `true`, the 99 | changelog will transform all issue links to links via a redirector 100 | (DuckDuckGo), to prevent GitHub from backreferencing the created PR in these 101 | issues. For more details, see 102 | https://github.com/knl/niv-updater-action/issues/26[Issue #26]. Defaults to 103 | `true`. 104 | * `debug_output`: (Optional, a boolean) If `true`, `set -x` will be turned on 105 | for the updater script, outputting every step the action takes. This will show 106 | up in the action log, and could be useful for trying to reproduce issues 107 | locally. Defaults to `false`. 108 | 109 | As the above list suggests, `niv-updater-action` is highly configurable. 110 | The following example exposes some of the knobs, many with their default values: 111 | 112 | [source,yaml] 113 | ---- 114 | name: Automated niv-managed dependency updates 115 | on: 116 | schedule: 117 | # * is a special character in YAML so you have to quote this string 118 | # run this every day at 4:00am 119 | - cron: '0 4 * * *' 120 | jobs: 121 | niv-updater: 122 | name: 'Create PRs for niv-managed dependencies' 123 | runs-on: ubuntu-latest 124 | steps: 125 | # notice there is no checkout step 126 | - name: niv-updater-action 127 | uses: knl/niv-updater-action@v15 128 | env: 129 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 130 | with: 131 | # NOTE: All inputs are optional. This list them with their default values. 132 | # Use the default branch for the repository 133 | pull_request_base: '' 134 | # The path in the repo to the sources.json file 135 | sources_file: 'nix/sources.json' 136 | # The niv version to use. `master` will track the latest niv. 137 | niv_version: 'master' 138 | # Keep the PR updated with new changes 139 | keep_updating: true 140 | # The prefix to add to every created branch 141 | branch_prefix: 'update/' 142 | # Update all dependencies tracked by niv. Another example: 'common,jq,hub' 143 | whitelist: '' 144 | # Do not blacklist any of the dependencies. Another example: 'nixpkgs,niv' 145 | blacklist: '' 146 | # Note that | is really important for the labels 147 | labels: | 148 | documentation 149 | good first issue 150 | # Have some prefix and a suffix. Use '|' to keep newlines 151 | message_prefix: | 152 | ## Motivation 153 | 154 | Dependencies should be up to date. 155 | message_suffix: 156 | Notify @myorg/myteam. 157 | # Have a prefix to the commit title itself, for example, to support conventional commits. 158 | title_prefix: refactor: 159 | ---- 160 | 161 | == Secrets 162 | 163 | Secrets are similar to inputs except that they are encrypted and only used by 164 | GitHub Actions. It's a convenient way to keep sensitive data out of the GitHub 165 | Actions workflow YAML file. 166 | 167 | * `GITHUB_TOKEN` - (Required) The GitHub API token used to create pull requests 168 | and get content from all repositories tracked by `niv`. 169 | 170 | == Self hosted runner 171 | 172 | Self-hosted runners are running with dynamic users so nix profile is not 173 | accessible, as well as nix-env. As this action relies on nix-env to install 174 | niv, the default configuration will not work. Thus, to use niv from available 175 | nixpkgs, set `niv_version` to `pass:[*from-nixpkgs*]`. It will install `niv` 176 | using `nixpkgs` with nix-shell instead of nix-env. 177 | 178 | To avoid using `sudo` (also unavailable on self-hosted runners), the input 179 | `pass:[skip_ssh_repos]` should be set to `true`. 180 | 181 | Example: 182 | 183 | [source,yaml] 184 | ---- 185 | name: Automated niv-managed dependency updates 186 | on: 187 | schedule: 188 | # * is a special character in YAML so you have to quote this string 189 | # run this every day at 4:00am 190 | - cron: '0 4 * * *' 191 | jobs: 192 | niv-updater: 193 | name: 'Create PRs for niv-managed dependencies' 194 | runs-on: self-hosted 195 | steps: 196 | # notice there is no checkout step 197 | - name: niv-updater-action 198 | uses: knl/niv-updater-action@v15 199 | with: 200 | niv_version: '*from-nixpkgs*' 201 | skip_ssh_repos: true 202 | ---- 203 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'niv Updater Action' 2 | description: 'A GitHub action that detects updates to dependencies tracked by niv and creates pull requests to keep them up to date.' 3 | author: 'knl' 4 | runs: 5 | using: 'node20' 6 | main: 'main.js' 7 | inputs: 8 | pull_request_base: 9 | description: 'The name of the branch to issue the pull requests against. Defaults to an empty string, which means to take the default branch for the repository.' 10 | required: false 11 | default: '' 12 | sources_file: 13 | description: 'The path in the repo to the sources.json file. This value will be passed to niv via `--sources-file` option. Defaults to `nix/sources.json`.' 14 | required: false 15 | default: 'nix/sources.json' 16 | niv_version: 17 | description: 'The niv version to be used. Defaults to `master`, meaning niv-updater-action will take the latest niv for each run. You may want to fix a particular version and avoid future breaks to your workflow.' 18 | required: false 19 | default: 'master' 20 | keep_updating: 21 | description: 'If PR already exists, keep it updated with new changes. The branch will be force updated, as this process keeps a single commit on a branch. Defaults to false to maintain the old behaviour.' 22 | required: false 23 | default: false 24 | branch_prefix: 25 | description: 'The prefix used for update branches, created by this action. The action does not sanitize the branch name. For a description of what a valid branch name is, please consult: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html. Defaults to "update/".' 26 | required: false 27 | default: 'update/' 28 | skip_versioned_revisions: 29 | description: 'If `true`, will cause the action to skip updating any dependency that has a version in their revision. This is due to the way niv currently works, where it will always update to the latest HEAD of a branch. Thus, if one have a dependency where, for example, `rev=v1.0.0`, niv would normally update it to the latest head of the branch, making `rev` holding the appropriate SHA. This is something one would not normally expect. Thus, this option exists until niv fixes the behaviour. Defaults to `true`.' 30 | required: false 31 | default: true 32 | skip_ssh_repos: 33 | description: 'If `true`, will cause the action to skip updating any dependency that is hosted by a repo accessible via ssh. Defaults to `false`.' 34 | required: false 35 | default: false 36 | whitelist: 37 | description: 'A list of dependencies, comma separated, that will be checked for updates. Other dependencies tracked by niv will not be checked for updates. This list will be consulted before the blacklist. It defaults to the list of all dependencies tracked by niv.' 38 | required: false 39 | default: '' 40 | blacklist: 41 | description: 'A list of dependencies, comma separated, to skip from updating. This list will be consulted after evaluating the whitelist.' 42 | required: false 43 | default: '' 44 | labels: 45 | description: 'A list of labels, *newline* separated, that will be applied to the generated PR. Defaults to an empty list.' 46 | required: false 47 | default: '' 48 | show_merges: 49 | description: 'If `true`, the changelog will contain merge commits listed. Otherwise, they will be skipped (however, the commits from the PRs/branches will shown). Defaults to `false`.' 50 | required: false 51 | default: false 52 | message_prefix: 53 | description: 'The text that will be put in front of the generated changelog. Defaults to empty.' 54 | required: false 55 | default: '' 56 | message_suffix: 57 | description: 'The text that will be put in after the generated changelog. Defaults to empty.' 58 | required: false 59 | default: '' 60 | title_prefix: 61 | description: 'The text that will be put in front of the generated commit title. Defaults to empty.' 62 | required: false 63 | default: '' 64 | github_changelog_no_backreferences: 65 | description: 'If `true`, the changelog will transform all issue links to links via a redirector, to prevent GitHub from backreferencing the created PR in these issues. Defaults to `true`.' 66 | required: false 67 | default: true 68 | debug_output: 69 | description: If `true`, `set -x` will be turned on for the updater script, outputing every step the action takes. This will show up in the action log, and could be useful for trying to reproduce issues locally. Defaults to `false`. 70 | required: false 71 | default: false 72 | 73 | branding: 74 | # maybe 'refresh-cw' 75 | icon: 'git-pull-request' 76 | color: 'orange' 77 | -------------------------------------------------------------------------------- /assets/niv-update-action-changelog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knl/niv-updater-action/5b63ba471a2acbde9fec87d8cfc123a4c0d5e210/assets/niv-update-action-changelog.png -------------------------------------------------------------------------------- /devenv.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devenv": { 4 | "locked": { 5 | "dir": "src/modules", 6 | "lastModified": 1729087493, 7 | "owner": "cachix", 8 | "repo": "devenv", 9 | "rev": "d612b77ff73912cd82e58256ab5e84d5904abef7", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "dir": "src/modules", 14 | "owner": "cachix", 15 | "repo": "devenv", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-compat": { 20 | "flake": false, 21 | "locked": { 22 | "lastModified": 1696426674, 23 | "owner": "edolstra", 24 | "repo": "flake-compat", 25 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "edolstra", 30 | "repo": "flake-compat", 31 | "type": "github" 32 | } 33 | }, 34 | "gitignore": { 35 | "inputs": { 36 | "nixpkgs": [ 37 | "pre-commit-hooks", 38 | "nixpkgs" 39 | ] 40 | }, 41 | "locked": { 42 | "lastModified": 1709087332, 43 | "owner": "hercules-ci", 44 | "repo": "gitignore.nix", 45 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "hercules-ci", 50 | "repo": "gitignore.nix", 51 | "type": "github" 52 | } 53 | }, 54 | "nixpkgs": { 55 | "locked": { 56 | "lastModified": 1728979988, 57 | "owner": "NixOS", 58 | "repo": "nixpkgs", 59 | "rev": "7881fbfd2e3ed1dfa315fca889b2cfd94be39337", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "owner": "NixOS", 64 | "ref": "nixpkgs-unstable", 65 | "repo": "nixpkgs", 66 | "type": "github" 67 | } 68 | }, 69 | "nixpkgs-stable": { 70 | "locked": { 71 | "lastModified": 1728909085, 72 | "owner": "NixOS", 73 | "repo": "nixpkgs", 74 | "rev": "c0b1da36f7c34a7146501f684e9ebdf15d2bebf8", 75 | "type": "github" 76 | }, 77 | "original": { 78 | "owner": "NixOS", 79 | "ref": "nixos-24.05", 80 | "repo": "nixpkgs", 81 | "type": "github" 82 | } 83 | }, 84 | "pre-commit-hooks": { 85 | "inputs": { 86 | "flake-compat": "flake-compat", 87 | "gitignore": "gitignore", 88 | "nixpkgs": [ 89 | "nixpkgs" 90 | ], 91 | "nixpkgs-stable": "nixpkgs-stable" 92 | }, 93 | "locked": { 94 | "lastModified": 1729104314, 95 | "owner": "cachix", 96 | "repo": "pre-commit-hooks.nix", 97 | "rev": "3c3e88f0f544d6bb54329832616af7eb971b6be6", 98 | "type": "github" 99 | }, 100 | "original": { 101 | "owner": "cachix", 102 | "repo": "pre-commit-hooks.nix", 103 | "type": "github" 104 | } 105 | }, 106 | "root": { 107 | "inputs": { 108 | "devenv": "devenv", 109 | "nixpkgs": "nixpkgs", 110 | "pre-commit-hooks": "pre-commit-hooks" 111 | } 112 | } 113 | }, 114 | "root": "root", 115 | "version": 7 116 | } 117 | -------------------------------------------------------------------------------- /devenv.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | 3 | { 4 | # https://devenv.sh/basics/ 5 | # env.GREET = "devenv"; 6 | 7 | # https://devenv.sh/packages/ 8 | packages = [ pkgs.asciidoctor ]; 9 | 10 | # enterShell = '' 11 | # ''; 12 | 13 | # https://devenv.sh/languages/ 14 | # languages.nix.enable = true; 15 | 16 | # https://devenv.sh/scripts/ 17 | # scripts.hello.exec = "echo hello from $GREET"; 18 | 19 | # https://devenv.sh/pre-commit-hooks/ 20 | pre-commit.hooks.shellcheck.enable = true; 21 | pre-commit.hooks.actionlint.enable = true; 22 | pre-commit.hooks.shfmt.enable = true; 23 | # pre-commit.hooks.typos.enable = true; 24 | 25 | # https://devenv.sh/processes/ 26 | # processes.ping.exec = "ping example.com"; 27 | } 28 | -------------------------------------------------------------------------------- /devenv.yaml: -------------------------------------------------------------------------------- 1 | inputs: 2 | nixpkgs: 3 | url: github:NixOS/nixpkgs/nixpkgs-unstable 4 | # can't point to the local modules here as it's used as a template -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { execFileSync } = require('child_process'); 2 | // just go straight to the good stuff 3 | execFileSync(`${__dirname}/niv-updater`, { stdio: 'inherit' }); 4 | -------------------------------------------------------------------------------- /niv-updater: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # vim: set ft=bash 3 | 4 | set -euo pipefail 5 | 6 | error() { 7 | echo "::error::$*" 8 | exit 1 9 | } 10 | 11 | warn() { 12 | echo "::warning::$*" 13 | } 14 | 15 | # Check if GITHUB_TOKEN is correct (maybe due to misconfiguration, we might get an expired one) 16 | checkCredentials() { 17 | if ! curl --fail -s -H "Authorization: token $GITHUB_TOKEN" 'https://api.github.com/'; then 18 | error 'GITHUB_TOKEN is incorrect, aborting' 19 | fi 20 | } 21 | 22 | # Install all the dependencies we need, using Nix 23 | setupPrerequisites() { 24 | echo 'Installing Nix' 25 | 26 | # Check if some other step already installed Nix 27 | if [[ ! -d /nix/store ]] || ! nix --version >/dev/null 2>&1; then 28 | # Create a temporary workdir 29 | workdir=$(mktemp -d) 30 | trap 'rm -rf "$workdir"' EXIT 31 | 32 | # Configure Nix 33 | add_config() { 34 | echo "$1" >>"$workdir/nix.conf" 35 | } 36 | 37 | # Workaround segfault: https://github.com/NixOS/nix/issues/2733 38 | add_config "http2 = false" 39 | 40 | add_config "max-jobs = auto" 41 | if [[ $OSTYPE =~ darwin ]]; then 42 | add_config "ssl-cert-file = /etc/ssl/cert.pem" 43 | fi 44 | # Allow binary caches for user 45 | add_config "trusted-users = root ${USER:-}" 46 | add_config "experimental-features = nix-command flakes" 47 | # Add a GitHub access token. 48 | # Token-less access is subject to lower rate limits. 49 | # Use the default GitHub token if available. 50 | # Skip this step if running an Enterprise instance. The default token there does not work for github.com. 51 | if [[ -n ${GITHUB_TOKEN:-} && $GITHUB_SERVER_URL == "https://github.com" ]]; then 52 | echo "::debug::Using the default GITHUB_TOKEN for github.com" 53 | add_config "access-tokens = github.com=$GITHUB_TOKEN" 54 | else 55 | echo "::debug::Continuing without a GitHub access token" 56 | fi 57 | # Nix installer flags 58 | installer_options=( 59 | --no-channel-add 60 | --darwin-use-unencrypted-nix-store-volume 61 | --nix-extra-conf-file "$workdir/nix.conf" 62 | --no-daemon 63 | ) 64 | 65 | # "fix" the following error when running nix* 66 | # error: the group 'nixbld' specified in 'build-users-group' does not exist 67 | add_config "build-users-group =" 68 | sudo mkdir -p /etc/nix 69 | sudo chmod 0755 /etc/nix 70 | sudo cp "$workdir/nix.conf" /etc/nix/nix.conf 71 | 72 | # There is --retry-on-errors, but only newer curl versions support that 73 | curl_retries=5 74 | # pin to ensure future upgrades do not interfere with the subsequent steps 75 | while ! curl -sS -o "$workdir/install" -v --fail -L "https://releases.nixos.org/nix/nix-2.24.9/install"; do 76 | sleep 1 77 | ((curl_retries--)) 78 | if [[ $curl_retries -le 0 ]]; then 79 | echo "curl retries failed" >&2 80 | exit 1 81 | fi 82 | done 83 | 84 | sh "$workdir/install" "${installer_options[@]}" 85 | 86 | # Set paths 87 | echo "/nix/var/nix/profiles/default/bin" >>"$GITHUB_PATH" 88 | # new path for nix 2.14 89 | echo "$HOME/.nix-profile/bin" >>"$GITHUB_PATH" 90 | fi 91 | 92 | echo 'Installing Nix - done' 93 | echo 'Installing dependencies' 94 | 95 | export PATH="${PATH}:/nix/var/nix/profiles/per-user/runner/profile/bin:/nix/var/nix/profiles/default/bin" 96 | 97 | # shellcheck disable=SC1091 98 | test -f "${HOME}/.nix-profile/etc/profile.d/nix.sh" && source "${HOME}/.nix-profile/etc/profile.d/nix.sh" 99 | 100 | PACKAGES=(jq moreutils curl) 101 | if [[ $INPUT_NIV_VERSION == "*from-nixpkgs*" ]]; then 102 | PACKAGES+=(niv) 103 | else 104 | # we also need hub, and git, but they both come with ubuntu-latest with GitHub Actions 105 | # https://github.com/actions/virtual-environments/blob/master/images/linux/Ubuntu1804-README.md 106 | # NOTE: We add 'cache.nixos.org' as a substituter because niv uses it as an upstream cache: 107 | # https://blog.cachix.org/posts/2020-07-28-upstream-caches-avoiding-pushing-paths-in-cache-nixos-org/ 108 | nix-env -iA niv -f "https://github.com/nmattia/niv/tarball/$INPUT_NIV_VERSION" \ 109 | --substituters 'https://niv.cachix.org https://cache.nixos.org' \ 110 | --trusted-public-keys 'niv.cachix.org-1:X32PCg2e/zAm3/uD1ScqW2z/K0LtDyNV7RdaxIuLgQM= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=' 111 | fi 112 | # We need --rawfile for jq, available in jq 1.6, which is only available on ubuntu 20.04 and macos 113 | # We need moreutils because of the sponge utility (to simplify the code) 114 | 115 | # Install if hub dependency is missing on self hosted runner 116 | [[ $(type -P "hub") ]] || PACKAGES+=(hub) 117 | 118 | # shellcheck disable=SC2016 119 | PATH="${PATH}:$(nix-shell -I "nixpkgs=https://github.com/NixOS/nixpkgs/archive/refs/heads/nixos-24.05.tar.gz" -p "${PACKAGES[@]}" --command 'echo $PATH')" 120 | 121 | echo 'Installing dependencies - done' 122 | } 123 | 124 | # Setup netrc file to be used by nix-prefetch-url 125 | setupNetrc() { 126 | if [[ $INPUT_SKIP_SSH_REPOS == 'false' ]]; then 127 | netrc=$(mktemp) 128 | sudo chmod 0600 "$netrc" 129 | sudo sh -c "echo 'netrc-file = $netrc' >> /etc/nix/nix.conf" 130 | cat <>"$netrc" 131 | machine github.com 132 | login api 133 | password $GITHUB_TOKEN 134 | EOF 135 | # shellcheck disable=SC2064 136 | trap "rm -f '$netrc'" EXIT 137 | fi 138 | } 139 | 140 | # This function will modify all INPUT_* variables so that they don't contain any garbage 141 | sanitizeInputs() { 142 | # remove all whitespace 143 | INPUT_WHITELIST="${INPUT_WHITELIST// /}" 144 | # remove all redundant commas, as well as those at the beginning and end 145 | shopt -s extglob 146 | INPUT_WHITELIST="${INPUT_WHITELIST//+(,)/,}" 147 | INPUT_WHITELIST="${INPUT_WHITELIST//^+(,)/}" 148 | INPUT_WHITELIST="${INPUT_WHITELIST//+(,)$/}" 149 | shopt -u extglob 150 | 151 | # prepare the blacklist 152 | # only need to remove spaces, commas are irrelevant 153 | INPUT_BLACKLIST="${INPUT_BLACKLIST// /}" 154 | 155 | # remove all empty lines from the labels 156 | # do not remove whitespace at the beginning and the end of the line, it might be legal 157 | shopt -s extglob 158 | INPUT_LABELS="${INPUT_LABELS##+(*([[:space:]])$'\n')}" 159 | INPUT_LABELS="${INPUT_LABELS%%+(*([[:space:]])$'\n')}" 160 | INPUT_LABELS="${INPUT_LABELS//+(*([[:space:]])$'\n')*([[:space:]])$'\n'/$'\n'}" 161 | shopt -s extglob 162 | } 163 | 164 | applyLabels() { 165 | if [[ -z $INPUT_LABELS ]]; then 166 | echo 'No labels to add' 167 | return 168 | fi 169 | 170 | echo 'Adding labels to the PR' 171 | 172 | local pr_data 173 | pr_data="$1" 174 | 175 | pr_number="$(jq -jr '.number // empty' <"$pr_data")" 176 | if [[ -z $pr_number ]]; then 177 | echo '::warning::could not get the PR number from the json payload, skipping adding labels' 178 | return 179 | fi 180 | 181 | if ! jq -n --arg labs "$INPUT_LABELS" '{labels: $labs | split("\n") }' \ 182 | | hub api -XPOST --input - \ 183 | "/repos/$GITHUB_REPOSITORY/issues/$pr_number/labels" >/dev/null; then 184 | echo "::warning::could not assign labels to the PR $pr_number" 185 | fi 186 | } 187 | 188 | # This function will turn any #123 into owner/repo#123. 189 | # This is needed, since the changelogs from different repositories will all have issue links as #123, 190 | # which, when printed as a PR description will create a link to the _current_ repository (one with 191 | # nix/sources.json), not one where the changelog originated from. The owner/repo#123 format will 192 | # ensure proper linking. 193 | formatIssueLinksPlain() { 194 | dep_owner="$1" 195 | dep_repo="$2" 196 | sed "s~\(#[0-9]\+\)~$dep_owner/$dep_repo\1~g" 197 | } 198 | 199 | # Like formatIssuesLinkPlain, this function turns any #123 a direct reference to the original repository. 200 | # Unlike formatIssuesLink, it will use a redirection service (togithub.com), so that referenced PRs/Issues 201 | # do not contain back references. 202 | # For more details, see https://github.com/knl/niv-updater-action/issues/26 203 | formatIssueLinksNoBackreferences() { 204 | dep_owner="$1" 205 | dep_repo="$2" 206 | # The sequence e2 81 a0 is \u2060 - UNICODE WORD JOINER 207 | # We need it in order prevent GitHub from making a backreference due to 208 | # the commit message text. 209 | sed "s~#\([0-9]\+\)~[$dep_owner/$dep_repo\xe2\x81\xa0#\1](https://togithub.com/$dep_owner/$dep_repo/issues/\1)~g" 210 | } 211 | 212 | # This function formats mentions in the generated changelog in such a way that 213 | # GitHub doesn't ping the mentioned person. This is to remove the noise we might 214 | # throw at our fellow developers. 215 | # Similarly to formatIssueLinksNoBackreferences, we add UNICODE WORD JOINER 216 | # between the '@' and the username 217 | formatMentions() { 218 | sed 's~\B@\([a-zA-Z0-9]\)~@\xe2\x81\xa0\1~g' 219 | } 220 | 221 | # A dispatcher for formatIssueLinksPlain and formatIssueLinksNoBackreferences, based on config. 222 | formatIssueLinks() { 223 | if [[ $INPUT_GITHUB_CHANGELOG_NO_BACKREFERENCES == 'true' ]]; then 224 | formatIssueLinksNoBackreferences "$@" 225 | else 226 | formatIssueLinksPlain "$@" 227 | fi 228 | } 229 | 230 | createPullRequestsOnUpdate() { 231 | echo 'Checking for updates' 232 | if [[ -z $INPUT_PULL_REQUEST_BASE ]]; then 233 | INPUT_PULL_REQUEST_BASE="${GITHUB_REF#refs/heads/}" 234 | INPUT_PULL_REQUEST_BASE="${INPUT_PULL_REQUEST_BASE#refs/tags/}" 235 | base="$GITHUB_SHA" 236 | else 237 | # get the SHA of the current base, so that it remains fixed during the run 238 | # This can fail if the branch doesn't exist. jq would return nothing in that case. 239 | set +euo pipefail 240 | base="$(hub api "/repos/$GITHUB_REPOSITORY/branches/$INPUT_PULL_REQUEST_BASE" | jq -jr '.commit.sha // empty')" 241 | if [[ -z $base ]]; then 242 | error "Could not get the SHA for branch '$INPUT_PULL_REQUEST_BASE'" 243 | fi 244 | set -euo pipefail 245 | fi 246 | 247 | echo "Will use branch '$INPUT_PULL_REQUEST_BASE' (ref: $base) as the base branch" 248 | 249 | merges_filter='' 250 | if [[ $INPUT_SHOW_MERGES == 'false' ]]; then 251 | # a filter for jq, to be used in the query for getting the changelog 252 | merges_filter='| select((.parents | length) < 2)' 253 | fi 254 | 255 | SOURCES_JSON='nix/sources.json' 256 | 257 | echo "Getting $INPUT_SOURCES_FILE from $GITHUB_REPOSITORY (ref: $base)" 258 | # get the content 259 | sj=$(mktemp) 260 | if ! hub api -XGET -F ref="$base" "/repos/$GITHUB_REPOSITORY/contents/$INPUT_SOURCES_FILE" >>"$sj"; then 261 | error 'could not fetch sources.json' 262 | fi 263 | echo "Getting $INPUT_SOURCES_FILE from $GITHUB_REPOSITORY (ref: $base) - done" 264 | 265 | file_sha="$(jq -jr '.sha' <"$sj")" 266 | content="$(jq -r '.content' <"$sj" | base64 -d)" 267 | 268 | if [[ -n $INPUT_WHITELIST ]]; then 269 | # Can't do <<< as it *appends* a newline :( 270 | mapfile -td , all_deps < <(printf '%s' "$INPUT_WHITELIST") 271 | else 272 | mapfile -t all_deps < <(jq -r 'keys[]' <<<"$content") 273 | fi 274 | 275 | echo 'Going through all dependencies' 276 | for dep in "${all_deps[@]}"; do 277 | echo "Processing dependency '$dep'" 278 | 279 | if [[ ",$INPUT_BLACKLIST," == *",$dep,"* ]]; then 280 | echo "Dependency '$dep' is blacklisted, skipping." 281 | continue 282 | fi 283 | 284 | revision="$(jq -jr ".\"$dep\".rev" <<<"$content")" 285 | 286 | # check if revision doesn't look like sha, and if skip_versioned_revisions is set, skip 287 | if [[ $INPUT_SKIP_VERSIONED_REVISIONS == 'true' && ! ($revision =~ ^[a-f0-9A-F]{40}$ || $revision =~ ^[a-f0-9A-F]{7}) ]]; then 288 | echo "Revision '$revision' looks like a regular version string, and skip_versioned_revisions is set, skipping." 289 | continue 290 | fi 291 | 292 | # check if the branch exists first. 293 | # If it exists and we're not updating the PR, then skip. 294 | branch_name="$INPUT_BRANCH_PREFIX$dep-$revision" 295 | 296 | branch_exists='no' 297 | if hub api "/repos/$GITHUB_REPOSITORY/branches/$branch_name" >/dev/null; then 298 | if [[ $INPUT_KEEP_UPDATING == 'true' ]]; then 299 | branch_exists='yes' 300 | else 301 | echo "branch '$branch_name' already exists, skipping." 302 | continue 303 | fi 304 | fi 305 | 306 | echo "Will use branch '$branch_name' for the possible update" 307 | 308 | # since this is an action, we don't have SSH keys available 309 | # but we do have tokens that are useful 310 | # check if the dependency is to github 311 | dep_owner="$(jq -jr ".\"$dep\".owner // empty" <<<"$content")" 312 | dep_repo="$(jq -jr ".\"$dep\".repo // empty" <<<"$content")" 313 | dep_url="$(jq -jr ".\"$dep\".url // empty" <<<"$content" | { grep github.com || true; })" 314 | # Here, we want to recognize the following URLs: 315 | # ((git+)?ssh://)?git@github.com(:)?[:/]owner/repo[.git]? 316 | # That is, optional scheme (ssh), followed by git@github.com, followed with optional port and either : or / 317 | github_ssh="$(jq -jr ".\"$dep\".repo // empty" <<<"$content" | { grep -E '^((git\+)?ssh://)?git@github.com(:[[:digit:]]+)?[:/]?' || true; })" 318 | [[ -n $dep_url || -n $github_ssh ]] && is_github='yes' || is_github='' 319 | 320 | # skip if github_ssh and skip_ssh_repos is in effect 321 | if [[ -n $github_ssh && $INPUT_SKIP_SSH_REPOS == 'true' ]]; then 322 | echo 'Hosted by a repository accessible over SSH, and skip_ssh_repos is set, skipping.' 323 | continue 324 | fi 325 | 326 | # try extracting the owner and the repo 327 | if [[ -n $github_ssh ]]; then 328 | # Here, we can be lenient. If niv already added some entries, we know they are correct. 329 | # Thus, we extract something that looks like 'owner/repo.git' from the end of the string. 330 | dep_owner="$(echo "$github_ssh" | perl -nle 'print $1 if m/[:\/]([\w-]{1,39})\/([\w_.-]+?)(?:\.git)?$/;')" 331 | dep_repo="$(echo "$github_ssh" | perl -nle 'print $2 if m/[:\/]([\w-]{1,39})\/([\w_.-]+?)(?:\.git)?$/;')" 332 | fi 333 | 334 | # check if there is an update by running niv 335 | wdir=$(mktemp -d) 336 | mkdir -p "$wdir/nix" 337 | echo "$content" >"$wdir/$SOURCES_JSON.orig" 338 | echo "$content" >"$wdir/$SOURCES_JSON" 339 | ( 340 | cd "$wdir" 341 | 342 | # rewrite the entry so that we can use token instead of SSH 343 | if [[ -n $github_ssh ]]; then 344 | echo 'As this is a dependency fetched with SSH, trying to switch to https type' 345 | niv_branch="$(jq -jr ".\"$dep\".branch // empty" <"$wdir/$SOURCES_JSON")" 346 | niv drop "$dep" 347 | niv add "$dep_owner/$dep_repo" -a rev="$revision" -a branch="$niv_branch" 348 | rm "$SOURCES_JSON.orig" 349 | cp "$SOURCES_JSON" "$SOURCES_JSON.orig" 350 | fi 351 | 352 | # TODO: make sure niv can update from tag to tag, instead of always using shas 353 | if ! niv update "$dep"; then 354 | echo "::warning:: Cannot update '$dep', skipping it" 355 | touch .skip 356 | fi 357 | ) 358 | # the only way to continue when using subshells 359 | if [[ -e "$wdir/.skip" ]]; then 360 | rm -rf "$wdir" 361 | continue 362 | fi 363 | 364 | if diff -q "$wdir/$SOURCES_JSON.orig" "$wdir/$SOURCES_JSON" &>/dev/null; then 365 | echo "There is no update for '$dep', skipping." 366 | continue 367 | fi 368 | 369 | new_revision="$(jq -jr ".\"$dep\".rev" <"$wdir/$SOURCES_JSON")" 370 | 371 | if [[ $revision == "$new_revision" ]]; then 372 | echo "The new version ($new_revision) is the same as old ($revision) for $dep, skipping" 373 | continue 374 | fi 375 | 376 | # since in the previous step we possibly changed from ssh to https, revert back 377 | ( 378 | cd "$wdir" 379 | 380 | # rewrite the entry so that we can use token instead of SSH 381 | if [[ -n $github_ssh ]]; then 382 | echo 'Reverting the dependency back to SSH' 383 | echo "$content" >"$wdir/$SOURCES_JSON" 384 | niv modify "$dep" -a rev="$new_revision" 385 | fi 386 | ) 387 | 388 | # generate the message 389 | title=$(mktemp) 390 | message=$(mktemp) 391 | 392 | printf 'Will generate the Pull Request message for '%s', update from %.8s to %.8s\n' "$dep" "$revision" "$new_revision" 393 | 394 | printf '%sniv %s: update %.8s -> %.8s' "$INPUT_TITLE_PREFIX${INPUT_TITLE_PREFIX:+ }" "$dep" "$revision" "$new_revision" >>"$title" 395 | 396 | # print with a new line appended, as yaml swallows those 397 | printf '%s%s' "$INPUT_MESSAGE_PREFIX" "${INPUT_MESSAGE_PREFIX:+$'\n'}" >>"$message" 398 | 399 | # get a short changelog if we're on github 400 | if [[ -z $is_github ]]; then 401 | # pretty sure this is not github 402 | echo "Dependency '$dep' isn't hosted on github.com, cannot fetch the changelog" >>"$message" 403 | else 404 | echo "Dependency '$dep' is hosted on github.com" 405 | 406 | { 407 | niv_branch="$(jq -jr ".\"$dep\".branch // empty" <"$wdir/$SOURCES_JSON")" 408 | printf '## Changelog for %s:\n' "$dep" 409 | printf 'Branch: %s\n' "$niv_branch" 410 | printf 'Commits: [%s/%s@%.8s...%.8s](https://github.com/%s/%s/compare/%s...%s)\n\n' "$dep_owner" "$dep_repo" "$revision" "$new_revision" "$dep_owner" "$dep_repo" "$revision" "$new_revision" 411 | { 412 | # In order for this to work, one has to use both paginate and per_page 413 | hub api --paginate "/repos/$dep_owner/$dep_repo/compare/${revision}...${new_revision}?per_page=100" || true 414 | } | jq -r '.commits[] '"$merges_filter"' | "* [`\(.sha[0:8])`](\(.html_url)) \(.commit.message | split("\n") | first)"' \ 415 | | formatIssueLinks "$dep_owner" "$dep_repo" \ 416 | | formatMentions 417 | } >>"$message" 418 | fi 419 | 420 | message_size=$(wc -c <"$message") 421 | truncation_string="* ... _the rest of the list is truncated due to the maximum length of the PR message on GitHub. Please take a look at the commit message._" 422 | max_message_size=$((65536 - ${#INPUT_MESSAGE_SUFFIX} - 1 - ${#truncation_string} - 1)) 423 | 424 | # This is now split in two different, interleaved actions: 425 | # - generating the commit message of any size 426 | # - generating the PR description with max size of 64k 427 | commit_message=$(mktemp) 428 | { 429 | cat "$title" 430 | printf '\n\n' 431 | cat "$message" 432 | # print with a new line appended, as yaml swallows those 433 | printf '%s%s' "$INPUT_MESSAGE_SUFFIX" "${INPUT_MESSAGE_SUFFIX:+$'\n'}" 434 | } >"$commit_message" 435 | 436 | if ((message_size > max_message_size)); then 437 | head <"$message" -c $max_message_size | sed '$d' | sponge "$message" 438 | printf "%s\n" "$truncation_string" >>"$message" 439 | fi 440 | # print with a new line appended, as yaml swallows those 441 | printf '%s%s' "$INPUT_MESSAGE_SUFFIX" "${INPUT_MESSAGE_SUFFIX:+$'\n'}" >>"$message" 442 | 443 | new_content=$(mktemp) 444 | base64 "$wdir/$SOURCES_JSON" >>"$new_content" 445 | 446 | # This is the behaviour when the branch doesn't exist 447 | if [[ $branch_exists == 'no' ]]; then 448 | # create the branch 449 | echo "Creating branch '$branch_name' for the update of '$dep'" 450 | hub_api_out=$(mktemp) 451 | if ! hub api \ 452 | -F ref="refs/heads/$branch_name" \ 453 | -F sha="$base" \ 454 | "/repos/$GITHUB_REPOSITORY/git/refs" >"$hub_api_out"; then 455 | error "could not create branch $branch_name for base $base, due to $(cat "$hub_api_out")" 456 | fi 457 | 458 | # upload the content 459 | echo "Uploading the new $INPUT_SOURCES_FILE" 460 | # https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#create-or-update-file-contents 461 | if ! hub api -XPUT \ 462 | -F message=@"$commit_message" \ 463 | -F content=@"$new_content" \ 464 | -F sha="$file_sha" \ 465 | -F branch="$branch_name" \ 466 | "/repos/$GITHUB_REPOSITORY/contents/$INPUT_SOURCES_FILE" >/dev/null; then 467 | # try to delete the branch if adding a new content fails 468 | hub api -XDELETE "/repos/$GITHUB_REPOSITORY/git/refs/heads/$branch_name" >/dev/null || true 469 | error "could not upload content to $branch_name" 470 | fi 471 | 472 | # create a PR, use API to avoid the need for a local checkout 473 | echo "Creating a PR for updating '$dep', branch name is '$branch_name'" 474 | pr_data=$(mktemp) 475 | if ! hub api -XPOST \ 476 | -F head="$branch_name" \ 477 | -F base="$INPUT_PULL_REQUEST_BASE" \ 478 | -F title=@"$title" \ 479 | -F body=@"$message" \ 480 | "/repos/$GITHUB_REPOSITORY/pulls" >>"$pr_data"; then 481 | # try to delete the branch 482 | hub api -XDELETE "/repos/$GITHUB_REPOSITORY/git/refs/heads/$branch_name" >/dev/null || true 483 | error 'could not create a PR' 484 | fi 485 | 486 | applyLabels "$pr_data" 487 | 488 | # cleanup 489 | rm -f "$pr_data" 490 | else 491 | # The branch already exists and we need to update it. 492 | # The update will actually be the force update, where we create a new tree and commit objects, 493 | # and then update the reference and the PR description. 494 | 495 | # The steps are: 496 | 497 | # 0. check if the file is the same and skip in that case 498 | current_content=$(mktemp) 499 | if ! hub api -XGET -F ref="$branch_name" "/repos/$GITHUB_REPOSITORY/contents/$INPUT_SOURCES_FILE" \ 500 | | jq -jr '.content' | base64 -d >"$current_content"; then 501 | # We can't just warn and proceed here, as subsequent calls to create a tree might fail 502 | # if the tree already exists 503 | error 'could not get the old branch content, aborting' 504 | fi 505 | 506 | # This is a trick to exit early if the content is the same 507 | # shellcheck disable=SC2034,SC2043 508 | for irrelevant in "run_once"; do 509 | if diff -q "$current_content" "$wdir/$SOURCES_JSON"; then 510 | echo 'files are the same, no need to update the PR' 511 | break 512 | fi 513 | # 1. get the base commit sha - done before the if statement 514 | 515 | # 2. get the base tree from the base commit sha 516 | base_tree="$(hub api "/repos/$GITHUB_REPOSITORY/git/commits/$base" | jq -jr '.tree.sha')" 517 | 518 | # 3a. create a new blob with the content 519 | blob_data=$(mktemp) 520 | if ! hub api -XPOST -f encoding=base64 \ 521 | -F content=@"$new_content" \ 522 | "/repos/$GITHUB_REPOSITORY/git/blobs" >"$blob_data"; then 523 | # There is nothing better to do 524 | error 'could not create a blob for the new content' 525 | fi 526 | 527 | # 3b. create a new tree with the file update 528 | tree_data=$(mktemp) 529 | # Per: https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-tree 530 | # one must not provide both sha and content 531 | if ! jq --arg path "$INPUT_SOURCES_FILE" --arg base_tree "$base_tree" \ 532 | '{base_tree: $base_tree, tree: [{path: $path, mode: "100644", type: "blob", content: null, sha}]}' \ 533 | <"$blob_data" \ 534 | | hub api -XPOST --input - \ 535 | "/repos/$GITHUB_REPOSITORY/git/trees" >>"$tree_data"; then 536 | # There is nothing better to do 537 | error 'could not create a new tree with updated content' 538 | fi 539 | 540 | # 4. create a new commit 541 | commit_data=$(mktemp) 542 | if ! jq -ncj \ 543 | --arg parent "$base" \ 544 | --arg tree "$(jq -jr '.sha' <"$tree_data")" \ 545 | --rawfile message "$commit_message" \ 546 | '{message: $message, tree: $tree, parents: [$parent]}' \ 547 | | hub api -XPOST \ 548 | --input - \ 549 | "/repos/$GITHUB_REPOSITORY/git/commits" \ 550 | >"$commit_data"; then 551 | # There is nothing better to do 552 | error 'could not create a new commit with PR updates' 553 | fi 554 | 555 | # 5. update reference to point to the new commit 556 | old_branch_sha="$(hub api "/repos/$GITHUB_REPOSITORY/git/ref/heads/$branch_name" | jq -jr '.object.sha')" 557 | if ! hub api -XPATCH \ 558 | -F force=true \ 559 | -F sha="$(jq -jr '.sha' <"$commit_data")" \ 560 | "/repos/$GITHUB_REPOSITORY/git/refs/heads/$branch_name" >/dev/null; then 561 | # There is nothing better to do 562 | error 'could not update the branch to point to the new commit' 563 | fi 564 | 565 | # 6. find the PR for the branch 566 | # The best way to do it is via GraphQL 567 | pr_number_data=$(mktemp) 568 | # shellcheck disable=SC2016 569 | if ! hub api graphql -f owner="${GITHUB_REPOSITORY%/*}" -f name="${GITHUB_REPOSITORY#*/}" -f branch="$branch_name" -f query=' 570 | query PrNumber($owner: String!, $name: String!, $branch: String!) { 571 | repository(owner: $owner, name: $name) { 572 | pullRequests(last: 1, states: OPEN, headRefName: $branch) { 573 | nodes { 574 | number 575 | } 576 | } 577 | } 578 | }' >"$pr_number_data"; then 579 | # There is nothing better to do 580 | error 'could not get the PR number for this branch' 581 | fi 582 | 583 | pr_number="$(jq '.data.repository.pullRequests.nodes[].number' <"$pr_number_data")" 584 | 585 | # 7. update PR description and title 586 | if ! hub api -XPATCH \ 587 | -F body=@"$message" \ 588 | -F title=@"$title" \ 589 | "/repos/$GITHUB_REPOSITORY/pulls/$pr_number" >/dev/null; then 590 | # It would be good to roll back here to the previous commit 591 | warn 'could not update the PR description, reverting to the previous commit' 592 | if ! hub api -XPATCH \ 593 | -F force=true \ 594 | -F sha="$old_branch_sha" \ 595 | "/repos/$GITHUB_REPOSITORY/git/refs/heads/$branch_name" >/dev/null; then 596 | # There is nothing better to do 597 | error 'could not revert the branch to point to the old commit' 598 | fi 599 | fi 600 | 601 | # cleanup 602 | rm -f "$tree_data" 603 | rm -f "$commit_data" 604 | rm -f "$pr_number_data" 605 | done 606 | 607 | # cleanup 608 | rm -f "$current_content" 609 | fi 610 | 611 | # cleanup 612 | rm -rf "$wdir" 613 | rm -f "$new_content" 614 | rm -f "$commit_message" 615 | rm -f "$message" 616 | rm -f "$title" 617 | 618 | echo "Processing dependency '$dep' - done" 619 | done 620 | rm -f "$sj" 621 | echo 'Checking for updates - done' 622 | } 623 | 624 | if [[ $INPUT_DEBUG_OUTPUT == 'true' ]]; then 625 | set -x 626 | export HUB_VERBOSE=true 627 | fi 628 | 629 | setupPrerequisites 630 | checkCredentials 631 | setupNetrc 632 | sanitizeInputs 633 | createPullRequestsOnUpdate 634 | --------------------------------------------------------------------------------