[mirror] Command-line application for uploading a site to a git-pages server

Compare changes

Choose any two refs to compare.

+19 -24
.forgejo/workflows/ci.yaml
··· 7 7 8 8 jobs: 9 9 check: 10 - runs-on: codeberg-small-lazy 10 + runs-on: debian-trixie 11 11 container: 12 - image: docker.io/library/node:24-trixie-slim@sha256:fcdfd7bcd8f641c8c76a8950343c73912d68ba341e8dd1074e663b784d3e76f4 12 + image: docker.io/library/node:24-trixie-slim@sha256:b05474903f463ce4064c09986525e6588c3e66c51b69be9c93a39fb359f883ce 13 13 steps: 14 14 - name: Check out source code 15 - uses: https://code.forgejo.org/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 15 + uses: https://code.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 16 16 - name: Set up toolchain 17 17 uses: https://code.forgejo.org/actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 18 18 with: ··· 34 34 # IMPORTANT: This workflow step will not work without the Releases unit enabled! 35 35 if: ${{ forge.ref == 'refs/heads/main' || startsWith(forge.event.ref, 'refs/tags/v') }} 36 36 needs: [check] 37 - runs-on: codeberg-small-lazy 37 + runs-on: debian-trixie 38 38 container: 39 - image: docker.io/library/node:24-trixie-slim@sha256:fcdfd7bcd8f641c8c76a8950343c73912d68ba341e8dd1074e663b784d3e76f4 39 + image: docker.io/library/node:24-trixie-slim@sha256:b05474903f463ce4064c09986525e6588c3e66c51b69be9c93a39fb359f883ce 40 40 steps: 41 41 - name: Check out source code 42 - uses: https://code.forgejo.org/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 42 + uses: https://code.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 43 43 - name: Set up toolchain 44 44 uses: https://code.forgejo.org/actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 45 45 with: ··· 70 70 package: 71 71 if: ${{ forge.ref == 'refs/heads/main' || startsWith(forge.event.ref, 'refs/tags/v') }} 72 72 needs: [check] 73 - runs-on: codeberg-small-lazy 73 + runs-on: debian-trixie 74 74 container: 75 - image: docker.io/library/node:24-trixie-slim@sha256:fcdfd7bcd8f641c8c76a8950343c73912d68ba341e8dd1074e663b784d3e76f4 75 + image: docker.io/library/node:24-trixie-slim@sha256:b05474903f463ce4064c09986525e6588c3e66c51b69be9c93a39fb359f883ce 76 76 steps: 77 77 - name: Install dependencies 78 78 run: | 79 79 apt-get -y update 80 - apt-get -y install buildah ca-certificates 80 + apt-get -y install ca-certificates buildah qemu-user-binfmt 81 81 - name: Check out source code 82 - uses: https://code.forgejo.org/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 82 + uses: https://code.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 83 83 - name: Build container 84 84 run: | 85 85 printf '[storage]\ndriver="vfs"\nrunroot="/run/containers/storage"\ngraphroot="/var/lib/containers/storage"\n' | tee /etc/containers/storage.conf 86 - buildah build --arch=amd64 --tag=container:${VER}-amd64 . 87 - buildah build --arch=arm64 --tag=container:${VER}-arm64 . 88 - buildah manifest create container:${VER} \ 89 - container:${VER}-amd64 \ 90 - container:${VER}-arm64 86 + buildah build --arch=amd64 --tag=container:amd64 87 + buildah build --arch=arm64 --tag=container:arm64 88 + buildah manifest create container container:amd64 container:arm64 91 89 env: 92 90 BUILDAH_ISOLATION: chroot 93 - VER: ${{ startsWith(forge.event.ref, 'refs/tags/v') && forge.ref_name || 'latest' }} 94 91 - if: ${{ forge.repository == 'git-pages/git-pages-cli' }} 95 92 name: Push container to Codeberg 96 93 run: | 97 - buildah login --authfile=/tmp/authfile-${FORGE}.json \ 94 + buildah login --authfile=/tmp/authfile.json \ 98 95 -u ${{ vars.PACKAGES_USER }} -p ${{ secrets.PACKAGES_TOKEN }} ${FORGE} 99 - buildah manifest push --authfile=/tmp/authfile-${FORGE}.json \ 100 - --all container:${VER} "docker://${FORGE}/${{ forge.repository }}:${VER/v/}" 96 + buildah manifest push --authfile=/tmp/authfile.json \ 97 + --all container "docker://${FORGE}/${{ forge.repository }}:${VER/v/}" 101 98 env: 102 - BUILDAH_ISOLATION: chroot 103 99 FORGE: codeberg.org 104 100 VER: ${{ startsWith(forge.event.ref, 'refs/tags/v') && forge.ref_name || 'latest' }} 105 101 - if: ${{ forge.repository == 'git-pages/git-pages-cli' }} 106 102 name: Push container to code.forgejo.org 107 103 run: | 108 - buildah login --authfile=/tmp/authfile-${FORGE}.json \ 104 + buildah login --authfile=/tmp/authfile.json \ 109 105 -u ${{ vars.PACKAGES_USER }} -p ${{ secrets.CFO_PACKAGES_TOKEN }} ${FORGE} 110 - buildah manifest push --authfile=/tmp/authfile-${FORGE}.json \ 111 - --all container:${VER} "docker://${FORGE}/${{ forge.repository }}:${VER/v/}" 106 + buildah manifest push --authfile=/tmp/authfile.json \ 107 + --all container "docker://${FORGE}/${{ forge.repository }}:${VER/v/}" 112 108 env: 113 - BUILDAH_ISOLATION: chroot 114 109 FORGE: code.forgejo.org 115 110 VER: ${{ startsWith(forge.event.ref, 'refs/tags/v') && forge.ref_name || 'latest' }}
+1 -1
Dockerfile
··· 1 - FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.25-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66fc5914e228d831bbb AS builder 1 + FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.25-alpine@sha256:ac09a5f469f307e5da71e766b0bd59c9c49ea460a528cc3e6686513d64a6f1fb AS builder 2 2 ARG TARGETOS TARGETARCH 3 3 RUN apk --no-cache add ca-certificates git 4 4 WORKDIR /build
+24
flake.lock
··· 18 18 "type": "github" 19 19 } 20 20 }, 21 + "gomod2nix": { 22 + "inputs": { 23 + "flake-utils": [ 24 + "flake-utils" 25 + ], 26 + "nixpkgs": [ 27 + "nixpkgs" 28 + ] 29 + }, 30 + "locked": { 31 + "lastModified": 1763982521, 32 + "narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=", 33 + "owner": "nix-community", 34 + "repo": "gomod2nix", 35 + "rev": "02e63a239d6eabd595db56852535992c898eba72", 36 + "type": "github" 37 + }, 38 + "original": { 39 + "owner": "nix-community", 40 + "repo": "gomod2nix", 41 + "type": "github" 42 + } 43 + }, 21 44 "nix-filter": { 22 45 "locked": { 23 46 "lastModified": 1757882181, ··· 52 75 "root": { 53 76 "inputs": { 54 77 "flake-utils": "flake-utils", 78 + "gomod2nix": "gomod2nix", 55 79 "nix-filter": "nix-filter", 56 80 "nixpkgs": "nixpkgs" 57 81 }
+22 -4
flake.nix
··· 3 3 nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 4 flake-utils.url = "github:numtide/flake-utils"; 5 5 nix-filter.url = "github:numtide/nix-filter"; 6 + 7 + gomod2nix = { 8 + url = "github:nix-community/gomod2nix"; 9 + inputs.nixpkgs.follows = "nixpkgs"; 10 + inputs.flake-utils.follows = "flake-utils"; 11 + }; 6 12 }; 7 13 8 14 outputs = ··· 11 17 nixpkgs, 12 18 flake-utils, 13 19 nix-filter, 14 - }: 20 + ... 21 + }@inputs: 15 22 flake-utils.lib.eachDefaultSystem ( 16 23 system: 17 24 let 18 - pkgs = nixpkgs.legacyPackages.${system}; 25 + pkgs = import nixpkgs { 26 + inherit system; 19 27 20 - git-pages-cli = pkgs.buildGo125Module { 28 + overlays = [ 29 + inputs.gomod2nix.overlays.default 30 + ]; 31 + }; 32 + 33 + git-pages-cli = pkgs.buildGoApplication { 21 34 pname = "git-pages-cli"; 22 35 version = "0"; 23 36 ··· 41 54 "-s -w" 42 55 ]; 43 56 44 - vendorHash = "sha256-5vjUhN3lCr41q91lOD7v0F9c6a8GJj7wBGnnzgFBhJU="; 57 + go = pkgs.go_1_25; 58 + modules = ./gomod2nix.toml; 45 59 }; 46 60 in 47 61 { ··· 50 64 devShells.default = pkgs.mkShell { 51 65 inputsFrom = [ 52 66 git-pages-cli 67 + ]; 68 + 69 + packages = with pkgs; [ 70 + gomod2nix 53 71 ]; 54 72 }; 55 73
+2 -2
go.mod
··· 3 3 go 1.25.0 4 4 5 5 require ( 6 - github.com/klauspost/compress v1.18.1 7 - github.com/spf13/pflag v1.0.10 8 6 github.com/google/uuid v1.6.0 7 + github.com/klauspost/compress v1.18.2 8 + github.com/spf13/pflag v1.0.10 9 9 )
+2 -2
go.sum
··· 1 1 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 2 2 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 - github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= 4 - github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= 3 + github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= 4 + github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= 5 5 github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 6 6 github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+12
gomod2nix.toml
··· 1 + schema = 3 2 + 3 + [mod] 4 + [mod."github.com/google/uuid"] 5 + version = "v1.6.0" 6 + hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw=" 7 + [mod."github.com/klauspost/compress"] 8 + version = "v1.18.2" 9 + hash = "sha256-mRa+6qEi5joqQao13ZFogmq67rOQzHCVbCCjKA+HKEc=" 10 + [mod."github.com/spf13/pflag"] 11 + version = "v1.0.10" 12 + hash = "sha256-uDPnWjHpSrzXr17KEYEA1yAbizfcsfo5AyztY2tS6ZU="
+119 -62
main.go
··· 2 2 3 3 import ( 4 4 "archive/tar" 5 + "bufio" 5 6 "bytes" 7 + "crypto" 6 8 "crypto/sha256" 9 + "encoding/hex" 7 10 "errors" 8 11 "fmt" 9 12 "io" ··· 12 15 "net/url" 13 16 "os" 14 17 "runtime/debug" 18 + "strconv" 15 19 "strings" 16 20 17 21 "github.com/google/uuid" ··· 39 43 var challengeFlag = pflag.Bool("challenge", false, "compute DNS challenge entry from password (output zone file record)") 40 44 var challengeBareFlag = pflag.Bool("challenge-bare", false, "compute DNS challenge entry from password (output bare TXT value)") 41 45 var uploadGitFlag = pflag.String("upload-git", "", "replace site with contents of specified git repository") 42 - var uploadDirFlag = pflag.String("upload-dir", "", "replace site with contents of specified directory") 43 - var deleteFlag = pflag.Bool("delete", false, "delete site") 46 + var uploadDirFlag = pflag.String("upload-dir", "", "replace whole site or a path with contents of specified directory") 47 + var deleteFlag = pflag.Bool("delete", false, "delete whole site or a path") 44 48 var debugManifestFlag = pflag.Bool("debug-manifest", false, "retrieve site manifest as ProtoJSON, for debugging") 45 49 var serverFlag = pflag.String("server", "", "hostname of server to connect to") 46 50 var pathFlag = pflag.String("path", "", "partially update site at specified path") 51 + var parentsFlag = pflag.Bool("parents", false, "create parent directories of --path") 47 52 var atomicFlag = pflag.Bool("atomic", false, "require partial updates to be atomic") 48 - var verboseFlag = pflag.Bool("verbose", false, "display more information for debugging") 49 - var versionFlag = pflag.Bool("version", false, "display version information") 53 + var incrementalFlag = pflag.Bool("incremental", false, "make --upload-dir only upload changed files") 54 + var verboseFlag = pflag.BoolP("verbose", "v", false, "display more information for debugging") 55 + var versionFlag = pflag.BoolP("version", "V", false, "display version information") 50 56 51 57 func singleOperation() bool { 52 58 operations := 0 ··· 72 78 operations++ 73 79 } 74 80 return operations == 1 81 + } 82 + 83 + func gitBlobSHA256(data []byte) string { 84 + h := crypto.SHA256.New() 85 + h.Write([]byte("blob ")) 86 + h.Write([]byte(strconv.FormatInt(int64(len(data)), 10))) 87 + h.Write([]byte{0}) 88 + h.Write(data) 89 + return hex.EncodeToString(h.Sum(nil)) 75 90 } 76 91 77 92 func displayFS(root fs.FS, prefix string) error { ··· 93 108 }) 94 109 } 95 110 96 - func archiveFS(writer io.Writer, root fs.FS, prefix string) (err error) { 111 + // It doesn't make sense to use incremental updates for very small files since the cost of 112 + // repeating a request to fill in a missing blob is likely to be higher than any savings gained. 113 + const incrementalSizeThreshold = 256 114 + 115 + func archiveFS(writer io.Writer, root fs.FS, prefix string, needBlobs []string) (err error) { 116 + requestedSet := make(map[string]struct{}) 117 + for _, hash := range needBlobs { 118 + requestedSet[hash] = struct{}{} 119 + } 97 120 zstdWriter, _ := zstd.NewWriter(writer) 98 121 tarWriter := tar.NewWriter(zstdWriter) 99 122 if err = fs.WalkDir(root, ".", func(name string, entry fs.DirEntry, err error) error { 100 123 if err != nil { 101 124 return err 102 125 } 103 - fileInfo, err := entry.Info() 104 - if err != nil { 105 - return err 106 - } 107 - var tarName string 126 + header := &tar.Header{} 127 + data := []byte{} 108 128 if prefix == "" && name == "." { 109 129 return nil 110 130 } else if name == "." { 111 - tarName = prefix 131 + header.Name = prefix 112 132 } else { 113 - tarName = prefix + name 133 + header.Name = prefix + name 114 134 } 115 - var file io.ReadCloser 116 - var linkTarget string 117 135 switch { 118 136 case entry.Type().IsDir(): 119 - name += "/" 137 + header.Typeflag = tar.TypeDir 138 + header.Name += "/" 120 139 case entry.Type().IsRegular(): 121 - if file, err = root.Open(name); err != nil { 140 + header.Typeflag = tar.TypeReg 141 + if data, err = fs.ReadFile(root, name); err != nil { 122 142 return err 123 143 } 124 - defer file.Close() 144 + if *incrementalFlag && len(data) > incrementalSizeThreshold { 145 + hash := gitBlobSHA256(data) 146 + if _, requested := requestedSet[hash]; !requested { 147 + header.Typeflag = tar.TypeSymlink 148 + header.Linkname = "/git/blobs/" + hash 149 + data = nil 150 + } 151 + } 125 152 case entry.Type() == fs.ModeSymlink: 126 - if linkTarget, err = fs.ReadLink(root, name); err != nil { 153 + header.Typeflag = tar.TypeSymlink 154 + if header.Linkname, err = fs.ReadLink(root, name); err != nil { 127 155 return err 128 156 } 129 157 default: 130 158 return errors.New("tar: cannot add non-regular file") 131 159 } 132 - header, err := tar.FileInfoHeader(fileInfo, linkTarget) 133 - if err != nil { 160 + header.Size = int64(len(data)) 161 + if err = tarWriter.WriteHeader(header); err != nil { 134 162 return err 135 163 } 136 - header.Name = tarName 137 - if err = tarWriter.WriteHeader(header); err != nil { 164 + if _, err = tarWriter.Write(data); err != nil { 138 165 return err 139 - } 140 - if file != nil { 141 - _, err = io.Copy(tarWriter, file) 142 166 } 143 167 return err 144 168 }); err != nil { ··· 153 177 return 154 178 } 155 179 180 + // Stream archive data without ever loading the entire working set into RAM. 181 + func streamArchiveFS(root fs.FS, prefix string, needBlobs []string) io.ReadCloser { 182 + reader, writer := io.Pipe() 183 + go func() { 184 + err := archiveFS(writer, root, prefix, needBlobs) 185 + if err != nil { 186 + writer.CloseWithError(err) 187 + } else { 188 + writer.Close() 189 + } 190 + }() 191 + return reader 192 + } 193 + 156 194 func makeWhiteout(path string) (reader io.Reader) { 157 195 buffer := &bytes.Buffer{} 158 196 tarWriter := tar.NewWriter(buffer) ··· 210 248 } 211 249 212 250 var request *http.Request 251 + var uploadDir *os.Root 213 252 switch { 214 253 case *challengeFlag || *challengeBareFlag: 215 254 if *passwordFlag == "" { ··· 241 280 request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 242 281 243 282 case *uploadDirFlag != "": 244 - uploadDirFS, err := os.OpenRoot(*uploadDirFlag) 283 + uploadDir, err = os.OpenRoot(*uploadDirFlag) 245 284 if err != nil { 246 285 fmt.Fprintf(os.Stderr, "error: invalid directory: %s\n", err) 247 286 os.Exit(1) 248 287 } 249 288 250 289 if *verboseFlag { 251 - err := displayFS(uploadDirFS.FS(), pathPrefix) 290 + err := displayFS(uploadDir.FS(), pathPrefix) 252 291 if err != nil { 253 292 fmt.Fprintf(os.Stderr, "error: %s\n", err) 254 293 os.Exit(1) 255 294 } 256 295 } 257 296 258 - // Stream archive data without ever loading the entire working set into RAM. 259 - reader, writer := io.Pipe() 260 - go func() { 261 - err = archiveFS(writer, uploadDirFS.FS(), pathPrefix) 262 - if err != nil { 263 - fmt.Fprintf(os.Stderr, "error: %s\n", err) 264 - os.Exit(1) 265 - } 266 - writer.Close() 267 - }() 268 - 269 297 if *pathFlag == "" { 270 - request, err = http.NewRequest("PUT", siteURL.String(), reader) 298 + request, err = http.NewRequest("PUT", siteURL.String(), nil) 271 299 } else { 272 - request, err = http.NewRequest("PATCH", siteURL.String(), reader) 300 + request, err = http.NewRequest("PATCH", siteURL.String(), nil) 273 301 } 274 302 if err != nil { 275 303 fmt.Fprintf(os.Stderr, "error: %s\n", err) 276 304 os.Exit(1) 277 305 } 306 + request.Body = streamArchiveFS(uploadDir.FS(), pathPrefix, []string{}) 278 307 request.ContentLength = -1 279 308 request.Header.Add("Content-Type", "application/x-tar+zstd") 309 + request.Header.Add("Accept", "application/vnd.git-pages.unresolved;q=1.0, text/plain;q=0.9") 310 + if *parentsFlag { 311 + request.Header.Add("Create-Parents", "yes") 312 + } else { 313 + request.Header.Add("Create-Parents", "no") 314 + } 280 315 281 316 case *deleteFlag: 282 317 if *pathFlag == "" { ··· 332 367 request.Header.Set("Host", siteURL.Host) 333 368 } 334 369 335 - response, err := http.DefaultClient.Do(request) 336 - if err != nil { 337 - fmt.Fprintf(os.Stderr, "error: %s\n", err) 338 - os.Exit(1) 339 - } 340 - if *verboseFlag { 341 - fmt.Fprintf(os.Stderr, "server: %s\n", response.Header.Get("Server")) 342 - } 343 - if *debugManifestFlag { 344 - if response.StatusCode == 200 { 345 - io.Copy(os.Stdout, response.Body) 346 - fmt.Fprintf(os.Stdout, "\n") 347 - } else { 348 - io.Copy(os.Stderr, response.Body) 370 + displayServer := *verboseFlag 371 + for { 372 + response, err := http.DefaultClient.Do(request) 373 + if err != nil { 374 + fmt.Fprintf(os.Stderr, "error: %s\n", err) 349 375 os.Exit(1) 350 376 } 351 - } else { // an update operation 352 - if response.StatusCode == 200 { 353 - fmt.Fprintf(os.Stdout, "result: %s\n", response.Header.Get("Update-Result")) 354 - io.Copy(os.Stdout, response.Body) 355 - } else { 356 - fmt.Fprintf(os.Stderr, "result: error\n") 357 - io.Copy(os.Stderr, response.Body) 358 - os.Exit(1) 377 + if displayServer { 378 + fmt.Fprintf(os.Stderr, "server: %s\n", response.Header.Get("Server")) 379 + displayServer = false 380 + } 381 + if *debugManifestFlag { 382 + if response.StatusCode == http.StatusOK { 383 + io.Copy(os.Stdout, response.Body) 384 + fmt.Fprintf(os.Stdout, "\n") 385 + } else { 386 + io.Copy(os.Stderr, response.Body) 387 + os.Exit(1) 388 + } 389 + } else { // an update operation 390 + if *verboseFlag { 391 + fmt.Fprintf(os.Stderr, "response: %d %s\n", 392 + response.StatusCode, response.Header.Get("Content-Type")) 393 + } 394 + if response.StatusCode == http.StatusUnprocessableEntity && 395 + response.Header.Get("Content-Type") == "application/vnd.git-pages.unresolved" { 396 + needBlobs := []string{} 397 + scanner := bufio.NewScanner(response.Body) 398 + for scanner.Scan() { 399 + needBlobs = append(needBlobs, scanner.Text()) 400 + } 401 + response.Body.Close() 402 + if *verboseFlag { 403 + fmt.Fprintf(os.Stderr, "incremental: need %d blobs\n", len(needBlobs)) 404 + } 405 + request.Body = streamArchiveFS(uploadDir.FS(), pathPrefix, needBlobs) 406 + continue // resubmit 407 + } else if response.StatusCode == http.StatusOK { 408 + fmt.Fprintf(os.Stdout, "result: %s\n", response.Header.Get("Update-Result")) 409 + io.Copy(os.Stdout, response.Body) 410 + } else { 411 + fmt.Fprintf(os.Stderr, "result: error\n") 412 + io.Copy(os.Stderr, response.Body) 413 + os.Exit(1) 414 + } 359 415 } 416 + break 360 417 } 361 418 }
+3 -1
renovate.json
··· 9 9 "lockFileMaintenance": { 10 10 "enabled": true, 11 11 "automerge": false 12 - } 12 + }, 13 + "semanticCommits": "disabled", 14 + "commitMessagePrefix": "[Renovate]" 13 15 }