├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── allcmd │ └── allcmd.go ├── archivecmd │ └── archive.go ├── buildcmd │ └── build.go ├── corecmd │ ├── core.go │ └── core_test.go └── releasecmd │ └── release.go ├── codecov.yml ├── go.mod ├── go.sum ├── go.work.dev ├── hugoreleaser.env ├── hugoreleaser.yaml ├── internal ├── archives │ ├── archive.go │ ├── archive_test.go │ ├── archiveformats │ │ ├── archiveformats.go │ │ └── archiveformats_test.go │ ├── build.go │ ├── renamer │ │ └── renamer.go │ ├── targz │ │ └── targz.go │ └── zip │ │ └── zip.go ├── builds │ └── macos_universal_binary.go ├── common │ ├── errorsh │ │ └── helpers.go │ ├── ioh │ │ └── files.go │ ├── logging │ │ ├── defaulthandler.go │ │ ├── helpers.go │ │ └── nocolourshandler.go │ ├── mapsh │ │ └── maps.go │ ├── matchers │ │ ├── glob.go │ │ ├── glob_test.go │ │ ├── matchers.go │ │ └── matchers_test.go │ └── templ │ │ ├── templ.go │ │ └── templ_test.go ├── config │ ├── archive_config.go │ ├── build_config.go │ ├── config.go │ ├── config_test.go │ ├── decode.go │ └── release_config.go ├── plugins │ ├── plugins.go │ └── plugintypes │ │ ├── plugintypes.go │ │ └── plugintypes_test.go └── releases │ ├── changelog │ ├── changelog.go │ └── changelog_test.go │ ├── checksums.go │ ├── checksums_test.go │ ├── client.go │ ├── fakeclient.go │ ├── github.go │ ├── releasetypes │ ├── releasetypes.go │ └── releasetypes_test.go │ └── retry.go ├── main.go ├── main_test.go ├── maintenance ├── go.mod ├── go.sum ├── readmetoc.go └── readmetoc_test.go ├── staticfiles ├── templates.go └── templates │ └── release-notes.gotmpl ├── test.sh ├── testscripts ├── commands │ ├── all.txt │ ├── build_and_archive.txt │ └── release.txt ├── misc │ ├── archive_alias_replacements.txt │ ├── archive_plugin_deb.txt │ ├── build_chunks.txt │ ├── build_macos_universal_binary.txt │ ├── envvars.txt │ ├── errors-release-duplicate-archive.txt │ ├── errors_common.txt │ ├── errors_invalid_config.txt │ ├── flag_quiet.txt │ ├── flag_try.txt │ ├── flags_in_env.txt │ ├── releasenotes-custom-template.txt │ ├── releasenotes-short.txt │ ├── releasenotes.txt │ └── segments.txt └── unfinished │ └── noop.txt └── watch_testscripts.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bep] -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ main ] 4 | pull_request: 5 | name: Test 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | go-version: [1.22.x, 1.23.x] 11 | platform: [ macos-latest, ubuntu-latest, windows-latest] 12 | runs-on: ${{ matrix.platform }} 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | - name: Install staticcheck 19 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 20 | shell: bash 21 | - name: Install golint 22 | run: go install golang.org/x/lint/golint@latest 23 | shell: bash 24 | - name: Update PATH 25 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 26 | shell: bash 27 | - name: Checkout code 28 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | with: 30 | fetch-depth: 0 31 | - name: Fmt 32 | if: matrix.platform != 'windows-latest' # :( 33 | run: "diff <(gofmt -d .) <(printf '')" 34 | shell: bash 35 | - name: Vet 36 | run: go vet ./... 37 | - name: Staticcheck 38 | run: staticcheck ./... 39 | #- name: Lint 40 | # run: golint ./... 41 | - name: Test 42 | run: go test -race ./... -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic 43 | - name: Upload coverage 44 | if: success() && matrix.platform == 'ubuntu-latest' 45 | run: | 46 | curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import # One-time step 47 | curl -Os https://uploader.codecov.io/latest/linux/codecov 48 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM 49 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig 50 | gpgv codecov.SHA256SUM.sig codecov.SHA256SUM 51 | shasum -a 256 -c codecov.SHA256SUM 52 | chmod +x codecov 53 | ./codecov 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | go.work 15 | 16 | .DS_Store 17 | 18 | go.work.sum 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | 23 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tests on Linux, MacOS and Windows](https://github.com/gohugoio/hugoreleaser/workflows/Test/badge.svg)](https://github.com/gohugoio/hugoreleaser/actions?query=workflow%3ATest) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugoreleaser)](https://goreportcard.com/report/github.com/gohugoio/hugoreleaser) 3 | [![codecov](https://codecov.io/gh/gohugoio/hugoreleaser/branch/main/graph/badge.svg?token=OWZ9RCAYWO)](https://codecov.io/gh/gohugoio/hugoreleaser) 4 | [![GoDoc](https://godoc.org/github.com/gohugoio/hugoreleaser?status.svg)](https://godoc.org/github.com/gohugoio/hugoreleaser) 5 | 6 | * [Configuration](#configuration) 7 | * [Configuration File](#configuration-file) 8 | * [Archive Aliases](#archive-aliases) 9 | * [Template Expansion](#template-expansion) 10 | * [Environment Variables](#environment-variables) 11 | * [Glob Matching](#glob-matching) 12 | * [Partitions](#partitions) 13 | * [Manual Partitioning](#manual-partitioning) 14 | * [Parallelism](#parallelism) 15 | * [Plugins](#plugins) 16 | * [Release Notes](#release-notes) 17 | * [Why another Go release tool?](#why-another-go-release-tool) 18 | 19 | ## Configuration 20 | 21 | ### Configuration File 22 | 23 | Hugoreleaser reads its main configuration from a file named `hugoreleaser.yaml` in the working directory. See [this project's configuration](./hugoreleaser.yaml) for an annotated example. 24 | 25 | 26 | ### Definitions 27 | 28 | Hugoreleaser supports YAML anchors and aliases, and has a reserved section for `definitions`. This is useful for defining common settings that can be reused in multiple places, e.g. 29 | 30 | ```yaml 31 | definitions: 32 | archive_type_zip: &archive_type_zip 33 | type: 34 | format: zip 35 | extension: .zip 36 | 37 | - goarch: amd64 38 | archives: 39 | - paths: 40 | - builds/**/windows/** 41 | archive_settings: *archive_type_zip 42 | ``` 43 | 44 | ### Archive Aliases 45 | 46 | See Hugo's use [here](TODO(bep)). 47 | 48 | ### Template Expansion 49 | 50 | Hugoreleaser supports Go template syntax in all fields with suffix `_template` (e.g. `name_template` used to create archive names). 51 | 52 | The data received in the template (e.g. the ".") is: 53 | 54 | | Field | Description | 55 | | ------------- | ------------- | 56 | | Project | The project name as defined in config. | 57 | | Tag | The tag as defined by the -tag flag. | 58 | | Goos | The current GOOS. | 59 | | Goarch | The current GOARCH. | 60 | 61 | In addition to Go's [built-ins](https://pkg.go.dev/text/template#hdr-Functions), we have added a small number of convenient template funcs: 62 | 63 | * `upper` 64 | * `lower` 65 | * `replace` (uses `strings.ReplaceAll`) 66 | * `trimPrefix` 67 | * `trimSuffix` 68 | 69 | With that, a name template may look like this: 70 | 71 | ```toml 72 | name_template = "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 73 | ``` 74 | 75 | ### Environment Variables 76 | 77 | The order of presedence for environment variables/flags: 78 | 79 | 1. Flags (e.g. `-tag`) 80 | 2. OS environment variables. 81 | 3. Environment variables defined in `hugoreleaser.env`. 82 | 83 | A `hugoreleaser.env` file will, if found in the current directory, be parsed and loaded into the environment of the running process. The format is simple, a text files of key-value-pairs on the form `KEY=value`, empty lines and lines starting with `#` is ignored: 84 | 85 | Environment variable expressions in `hugoreleaser.yaml` on the form `${VAR}` will be expanded before it's parsed. 86 | 87 | An example `hugoreleaser.env` with the enviromnent for the next release may look like this: 88 | 89 | ``` 90 | HUGORELEASER_TAG=v1.2.3 91 | HUGORELEASER_COMMITISH=main 92 | MYPROJECT_RELEASE_NAME=First Release! 93 | MYPROJECT_RELEASE_DRAFT=false 94 | ``` 95 | 96 | In the above, the variables prefixed `HUGORELEASER_` will be used to set the flags when running the `hugoreleaser` commands. 97 | 98 | The other custom variables can be used in `hugoreleaser.yaml`, e.g: 99 | 100 | ```toml 101 | release_settings: 102 | name: ${MYPROJECT_RELEASE_NAME} 103 | ``` 104 | 105 | Note the special `@U` (_Unquoute_) syntax. The field `draft` is a boolean and cannot be quouted, but this would create ugly validation errors in TOML aware editors. The construct above signals that the quoutes (single or double) should be removed before doing any variable expansion. 106 | 107 | ## Glob Matching 108 | 109 | Hugo releaser supports the Glob rules as defined in [Gobwas Glob](https://github.com/gobwas/glob) with one additional rule: Glob patterns can be negated with a `!` prefix. 110 | 111 | The CLI `-paths` flag is a slice an, if repeated for a given prefix, will be ANDed together, e.g.: 112 | 113 | ``` 114 | hugoreleaser build -paths "builds/**" -paths "!builds/**/arm64" 115 | ``` 116 | 117 | The above will build everything, expect the ARM64 `GOARCH`. 118 | 119 | ## Partitions 120 | 121 | ### Manual Partitioning 122 | 123 | The configuration file and the (mimics the directory structure inside `/dist`) creates a simple tree structure that can be used to partition a build/release. All commands takes one or more `-paths` flag values. This is a [Glob Path](#glob-matching) matching builds to cover or releases to release (the latter is only relevant for the last step). Hugo has partitioned its builds using a container name as the first path element. With that, releasing may look something like this: 124 | 125 | ``` 126 | # Run this in container1 127 | hugoreleaser build --paths "builds/container1/**" 128 | # Run this in container2, using the same /dist as the first step. 129 | hugoreleaser build --paths "builds/container2/**" 130 | hugoreleaser archive 131 | hugoreleaser release 132 | ``` 133 | 134 | ### Parallelism 135 | 136 | The build command takes the optional `-chunks` and `-chunk-index` which could be used to automatically split the builds to speed up pipelines., e.g. using [Circle CI's Job Splitting](https://circleci.com/docs/parallelism-faster-jobs#using-environment-variables-to-split-tests). 137 | 138 | See [Hugo v0.102.0 Release Notes](https://github.com/gohugoio/hugo/releases/tag/v0.102.0) for more information. 139 | 140 | ## Plugins 141 | 142 | Hugoreleaser supports [Go Module](https://go.dev/blog/using-go-modules) plugins to create archives. See the [Deb Plugin](https://github.com/gohugoio/hugoreleaser-archive-plugins/tree/main/deb) for an example. 143 | 144 | See the [Hugoreleaser Plugins API](https://github.com/gohugoio/hugoreleaser-plugins-api) for API and more information. 145 | 146 | ## Release Notes 147 | 148 | The config map `release_notes_settings` has 3 options for how to handle release notes: 149 | 150 | 1. Set a `filename` 151 | 2. Set `generate_on_host=true` and let GitHub do it. 152 | 3. Set `generate=true` and let Hugoreleaser do it. 153 | 154 | There are more details about change grouping etc. in this [this project's configuration](./hugoreleaser.yaml). 155 | 156 | For the third option, you can set a custom release notes template to use in `template_filename`. See the default template in [staticfiles/templates/release-notes.gotmpl](./staticfiles/templates/release-notes.gotmpl) for an example. 157 | 158 | ## Why another Go release tool? 159 | 160 | This project was created because [Hugo](https://github.com/gohugoio/hugo) had some issues that seemed unsolvable with Goreleaser: 161 | 162 | * We had a CI release that timed out a lot (1 hour). And since Goreleaser creates the tag as the first step, we often ended up having to delete the tag and start over, creating all sorts of issues. 163 | * We wanted to add more build variants, but we couldn't. 164 | 165 | Hugo has used this tool for all of its releases since [v0.102.0](https://github.com/gohugoio/hugo/releases/tag/v0.102.0), and the release time has gone down from 50-60 minutes to around 10 minutes. 166 | -------------------------------------------------------------------------------- /cmd/allcmd/allcmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package allcmd 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | 21 | "github.com/gohugoio/hugoreleaser/cmd/archivecmd" 22 | "github.com/gohugoio/hugoreleaser/cmd/buildcmd" 23 | "github.com/gohugoio/hugoreleaser/cmd/corecmd" 24 | "github.com/gohugoio/hugoreleaser/cmd/releasecmd" 25 | 26 | "github.com/bep/logg" 27 | "github.com/peterbourgon/ff/v3/ffcli" 28 | ) 29 | 30 | const commandName = "all" 31 | 32 | // New returns a usable ffcli.Command for the archive subcommand. 33 | func New(core *corecmd.Core) *ffcli.Command { 34 | fs := flag.NewFlagSet(corecmd.CommandName+" "+commandName, flag.ExitOnError) 35 | 36 | builder := buildcmd.NewBuilder(core, fs) 37 | a := &all{ 38 | core: core, 39 | builder: builder, 40 | archivist: archivecmd.NewArchivist(core), 41 | releaser: releasecmd.NewReleaser(core, fs), 42 | } 43 | 44 | core.RegisterFlags(fs) 45 | 46 | return &ffcli.Command{ 47 | Name: commandName, 48 | ShortUsage: corecmd.CommandName + " " + commandName + " [flags] ", 49 | ShortHelp: "Runs the commands build, archive and release in sequence.", 50 | FlagSet: fs, 51 | Exec: a.Exec, 52 | } 53 | } 54 | 55 | type all struct { 56 | infoLog logg.LevelLogger 57 | core *corecmd.Core 58 | 59 | builder *buildcmd.Builder 60 | archivist *archivecmd.Archivist 61 | releaser *releasecmd.Releaser 62 | } 63 | 64 | func (a *all) Init() error { 65 | a.infoLog = a.core.InfoLog.WithField("all", commandName) 66 | return nil 67 | } 68 | 69 | func (a *all) Exec(ctx context.Context, args []string) error { 70 | if err := a.Init(); err != nil { 71 | return err 72 | } 73 | 74 | commandHandlers := []corecmd.CommandHandler{ 75 | a.builder, 76 | a.archivist, 77 | a.releaser, 78 | } 79 | 80 | for _, commandHandler := range commandHandlers { 81 | if err := commandHandler.Init(); err != nil { 82 | return err 83 | } 84 | } 85 | 86 | for _, commandHandler := range commandHandlers { 87 | if err := commandHandler.Exec(ctx, args); err != nil { 88 | return err 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /cmd/archivecmd/archive.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package archivecmd 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "fmt" 21 | "os" 22 | "path" 23 | "path/filepath" 24 | 25 | "github.com/gohugoio/hugoreleaser-plugins-api/archiveplugin" 26 | "github.com/gohugoio/hugoreleaser/cmd/corecmd" 27 | "github.com/gohugoio/hugoreleaser/internal/archives" 28 | "github.com/gohugoio/hugoreleaser/internal/config" 29 | "github.com/gohugoio/hugoreleaser/internal/plugins" 30 | 31 | "github.com/bep/helpers/filehelpers" 32 | "github.com/bep/logg" 33 | "github.com/gohugoio/hugoreleaser-plugins-api/model" 34 | "github.com/peterbourgon/ff/v3/ffcli" 35 | ) 36 | 37 | const commandName = "archive" 38 | 39 | // New returns a usable ffcli.Command for the archive subcommand. 40 | func New(core *corecmd.Core) *ffcli.Command { 41 | fs := flag.NewFlagSet(corecmd.CommandName+" "+commandName, flag.ExitOnError) 42 | 43 | archivist := NewArchivist(core) 44 | 45 | core.RegisterFlags(fs) 46 | 47 | return &ffcli.Command{ 48 | Name: "archive", 49 | ShortUsage: corecmd.CommandName + " archive [flags] ", 50 | ShortHelp: "Build archives from binaries and any extra files configured.", 51 | FlagSet: fs, 52 | Exec: archivist.Exec, 53 | } 54 | } 55 | 56 | type Archivist struct { 57 | infoLog logg.LevelLogger 58 | core *corecmd.Core 59 | } 60 | 61 | // NewArchivist returns a new Archivist. 62 | func NewArchivist(core *corecmd.Core) *Archivist { 63 | return &Archivist{ 64 | core: core, 65 | } 66 | } 67 | 68 | func (b *Archivist) Init() error { 69 | b.infoLog = b.core.InfoLog.WithField("cmd", commandName) 70 | c := b.core 71 | 72 | startAndRegister := func(p config.Plugin) error { 73 | if p.IsZero() { 74 | return nil 75 | } 76 | if _, found := c.PluginsRegistryArchive[p.ID]; found { 77 | // Already started. 78 | return nil 79 | } 80 | infoCtx := c.InfoLog.WithField("cmd", fmt.Sprintf("%s %s", commandName, p.ID)) 81 | cfg := plugins.ArchivePluginConfig{ 82 | Infol: infoCtx, 83 | Try: c.Try, 84 | GoSettings: c.Config.GoSettings, 85 | Options: p, 86 | Project: c.Config.Project, 87 | Tag: c.Tag, 88 | } 89 | client, err := plugins.StartArchivePlugin(cfg) 90 | if err != nil { 91 | return fmt.Errorf("error starting archive plugin %q: %w", p.ID, err) 92 | } 93 | 94 | infoCtx.Log(logg.String("Archive plugin started and ready for use")) 95 | c.PluginsRegistryArchive[p.ID] = client 96 | return nil 97 | } 98 | 99 | if err := startAndRegister(c.Config.ArchiveSettings.Plugin); err != nil { 100 | return err 101 | } 102 | for _, archive := range c.Config.Archives { 103 | if err := startAndRegister(archive.ArchiveSettings.Plugin); err != nil { 104 | return err 105 | } 106 | } 107 | return nil 108 | } 109 | 110 | func (b *Archivist) Exec(ctx context.Context, args []string) error { 111 | if err := b.Init(); err != nil { 112 | return err 113 | } 114 | 115 | r, _ := b.core.Workforce.Start(ctx) 116 | 117 | archiveDistDir := filepath.Join( 118 | b.core.DistDir, 119 | b.core.Config.Project, 120 | b.core.Tag, 121 | b.core.DistRootArchives, 122 | ) 123 | filter := b.core.PathsBuildsCompiled 124 | 125 | for _, archive := range b.core.Config.Archives { 126 | archive := archive 127 | for _, archPath := range archive.ArchsCompiled { 128 | if !filter.Match(archPath.Path) { 129 | continue 130 | } 131 | archPath := archPath 132 | archiveSettings := archive.ArchiveSettings 133 | arch := archPath.Arch 134 | goInfo := model.GoInfo{ 135 | Goos: arch.Os.Goos, 136 | Goarch: arch.Goarch, 137 | } 138 | 139 | r.Run(func() (err error) { 140 | outDir := filepath.Join(archiveDistDir, filepath.FromSlash(archPath.Path)) 141 | 142 | outFilename := filepath.Join( 143 | outDir, 144 | archPath.Name, 145 | ) 146 | 147 | b.infoLog.WithField("file", outFilename).Log(logg.String("Archive")) 148 | 149 | if b.core.Try { 150 | return nil 151 | } 152 | 153 | binaryFilename := filepath.Join( 154 | b.core.DistDir, 155 | b.core.Config.Project, 156 | b.core.Tag, 157 | b.core.DistRootBuilds, 158 | arch.BinaryPath(), 159 | ) 160 | 161 | binFi, err := os.Stat(binaryFilename) 162 | if err != nil { 163 | return fmt.Errorf("%s: binary file not found: %q", commandName, binaryFilename) 164 | } 165 | 166 | if err := os.MkdirAll(filepath.Dir(outFilename), 0o755); err != nil { 167 | return err 168 | } 169 | 170 | buildRequest := archiveplugin.Request{ 171 | GoInfo: goInfo, 172 | Settings: archiveSettings.CustomSettings, 173 | OutFilename: outFilename, 174 | } 175 | 176 | buildRequest.Files = append(buildRequest.Files, archiveplugin.ArchiveFile{ 177 | SourcePathAbs: binaryFilename, 178 | TargetPath: path.Join(archiveSettings.BinaryDir, arch.BuildSettings.Binary), 179 | Mode: binFi.Mode(), 180 | }) 181 | 182 | for _, extraFile := range archiveSettings.ExtraFiles { 183 | buildRequest.Files = append(buildRequest.Files, archiveplugin.ArchiveFile{ 184 | SourcePathAbs: filepath.Join(b.core.ProjectDir, extraFile.SourcePath), 185 | TargetPath: extraFile.TargetPath, 186 | Mode: extraFile.Mode, 187 | }) 188 | } 189 | 190 | err = archives.Build( 191 | b.core, 192 | b.infoLog, 193 | archiveSettings, 194 | buildRequest, 195 | ) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | for _, alias := range archPath.Aliases { 201 | aliasFilename := filepath.Join( 202 | outDir, 203 | alias, 204 | ) 205 | b.infoLog.WithField("file", aliasFilename).Log(logg.String("Alias")) 206 | if err := filehelpers.CopyFile(outFilename, aliasFilename); err != nil { 207 | return err 208 | } 209 | } 210 | 211 | return nil 212 | }) 213 | 214 | } 215 | } 216 | 217 | return r.Wait() 218 | } 219 | -------------------------------------------------------------------------------- /cmd/buildcmd/build.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package buildcmd 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "flag" 21 | "fmt" 22 | "math/rand" 23 | "os" 24 | "path/filepath" 25 | 26 | "github.com/bep/helpers/envhelpers" 27 | "github.com/bep/helpers/slicehelpers" 28 | 29 | "github.com/bep/logg" 30 | "github.com/gohugoio/hugoreleaser/cmd/corecmd" 31 | "github.com/gohugoio/hugoreleaser/internal/builds" 32 | "github.com/gohugoio/hugoreleaser/internal/config" 33 | "github.com/peterbourgon/ff/v3/ffcli" 34 | ) 35 | 36 | const commandName = "build" 37 | 38 | // From Go 1.20 the math/rand package started to seed the global random number generator (used by top-level functions like Float64 and Int) with a random value. 39 | // This broke chunking logic in Hugoreleaser. We need the same shuffle for each build. 40 | var rand1 = rand.New(rand.NewSource(1)) 41 | 42 | // New returns a usable ffcli.Command for the build subcommand. 43 | func New(core *corecmd.Core) *ffcli.Command { 44 | fs := flag.NewFlagSet(corecmd.CommandName+" "+commandName, flag.ExitOnError) 45 | core.RegisterFlags(fs) 46 | builder := NewBuilder(core, fs) 47 | 48 | return &ffcli.Command{ 49 | Name: "build", 50 | ShortUsage: corecmd.CommandName + " build [flags] ", 51 | ShortHelp: "Build Go binaries.", 52 | FlagSet: fs, 53 | Exec: builder.Exec, 54 | } 55 | } 56 | 57 | // NewBuilder returns a new Builder. 58 | func NewBuilder(core *corecmd.Core, fs *flag.FlagSet) *Builder { 59 | b := &Builder{ 60 | core: core, 61 | } 62 | 63 | fs.IntVar(&b.chunks, "chunks", -1, "Number of chunks to split the build into (optional).") 64 | fs.IntVar(&b.chunkIndex, "chunk-index", -1, "Index of the chunk to build (optional).") 65 | 66 | return b 67 | } 68 | 69 | type Builder struct { 70 | core *corecmd.Core 71 | infoLog logg.LevelLogger 72 | 73 | chunks int 74 | chunkIndex int 75 | } 76 | 77 | func (b *Builder) Init() error { 78 | b.infoLog = b.core.InfoLog.WithField("cmd", commandName) 79 | 80 | if b.chunks > 0 && b.chunkIndex >= b.chunks { 81 | return fmt.Errorf("chunk-index (%d) must be less than chunks (%d)", b.chunkIndex, b.chunks) 82 | } 83 | if b.chunks > 0 && b.chunkIndex < 0 { 84 | return fmt.Errorf("chunks (%d) requires chunk-index to be set", b.chunks) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (b *Builder) Exec(ctx context.Context, args []string) error { 91 | if err := b.Init(); err != nil { 92 | return err 93 | } 94 | 95 | if !b.core.Try { 96 | // Prevarm the GOMODCACHE if this is a Go module project. 97 | if _, err := os.Stat(filepath.Join(b.core.ProjectDir, "go.mod")); err == nil { 98 | b.infoLog.Log(logg.String("Running 'go mod download'.")) 99 | var buff bytes.Buffer 100 | if err := b.core.RunGo(ctx, nil, []string{"mod", "download"}, &buff); err != nil { 101 | b.core.ErrorLog.Log(logg.String(buff.String())) 102 | return err 103 | } 104 | } 105 | } 106 | 107 | archs := b.core.Config.FindArchs(b.core.PathsBuildsCompiled) 108 | 109 | if b.chunks > 0 { 110 | // Resource-heavy builds tend to be configured in proximity to eachother, 111 | // so this shuffle may help avoid clustering these slow builds in the same partition. 112 | rand1.Shuffle(len(archs), func(i, j int) { 113 | archs[i], archs[j] = archs[j], archs[i] 114 | }) 115 | 116 | partitions := slicehelpers.Chunk(archs, b.chunks) 117 | if len(partitions) <= b.chunkIndex { 118 | archs = nil 119 | b.infoLog.Logf("No GOOS/GOARCHs available for chunk %d of %d.", b.chunkIndex+1, b.chunks) 120 | } else { 121 | archs = partitions[b.chunkIndex] 122 | b.infoLog.Logf("Building %d GOOS/GOARCHs in chunk %d of %d.", len(archs), b.chunkIndex+1, b.chunks) 123 | } 124 | } else { 125 | b.infoLog.Logf("Building %d GOOS/GOARCHs.", len(archs)) 126 | } 127 | 128 | if len(archs) == 0 { 129 | return nil 130 | } 131 | 132 | r, ctx := b.core.Workforce.Start(ctx) 133 | 134 | for _, archPath := range archs { 135 | // Capture this for the Go routine below. 136 | archPath := archPath 137 | r.Run(func() error { 138 | return b.buildArch(ctx, archPath) 139 | }) 140 | } 141 | 142 | return r.Wait() 143 | } 144 | 145 | func (b *Builder) buildArch(ctx context.Context, archPath config.BuildArchPath) error { 146 | arch := archPath.Arch 147 | outDir := filepath.Join( 148 | b.core.DistDir, 149 | b.core.Config.Project, 150 | b.core.Tag, 151 | b.core.DistRootBuilds, 152 | filepath.FromSlash(archPath.Path), 153 | ) 154 | 155 | outFilename := filepath.Join( 156 | outDir, 157 | arch.BuildSettings.Binary, 158 | ) 159 | 160 | b.infoLog.WithField("binary", outFilename).WithFields(b.core.Config.BuildSettings).Log(logg.String("Building")) 161 | 162 | if b.core.Try { 163 | return nil 164 | } 165 | 166 | buildSettings := arch.BuildSettings 167 | 168 | buildBinary := func(filename, goarch string) error { 169 | var keyVals []string 170 | args := []string{"build", "-o", filename} 171 | 172 | keyVals = append( 173 | keyVals, 174 | "GOOS", arch.Os.Goos, 175 | "GOARCH", goarch, 176 | ) 177 | 178 | if buildSettings.Env != nil { 179 | for _, env := range buildSettings.Env { 180 | key, val := envhelpers.SplitEnvVar(env) 181 | keyVals = append(keyVals, key, val) 182 | } 183 | } 184 | 185 | if buildSettings.Ldflags != "" { 186 | args = append(args, "-ldflags", buildSettings.Ldflags) 187 | } 188 | if buildSettings.Flags != nil { 189 | args = append(args, buildSettings.Flags...) 190 | } 191 | 192 | return b.core.RunGo(ctx, keyVals, args, os.Stderr) 193 | } 194 | 195 | if arch.Goarch == builds.UniversalGoarch { 196 | // Build for both arm64 and amd64 and then combine them into a universal binary. 197 | goarchs := []string{"arm64", "amd64"} 198 | var outFilenames []string 199 | for _, goarch := range goarchs { 200 | filename := outFilename + "_" + goarch 201 | outFilenames = append(outFilenames, filename) 202 | if err := buildBinary(filename, goarch); err != nil { 203 | return err 204 | } 205 | } 206 | b.infoLog.Logf("Combining %v into a universal binary.", goarchs) 207 | if err := builds.CreateMacOSUniversalBinary(outFilename, outFilenames...); err != nil { 208 | return err 209 | } 210 | 211 | // Remove the individual binary files. 212 | for _, filename := range outFilenames { 213 | if err := os.Remove(filename); err != nil { 214 | return err 215 | } 216 | } 217 | 218 | } else { 219 | if err := buildBinary(outFilename, arch.Goarch); err != nil { 220 | return err 221 | } 222 | } 223 | 224 | return nil 225 | } 226 | -------------------------------------------------------------------------------- /cmd/corecmd/core_test.go: -------------------------------------------------------------------------------- 1 | package corecmd 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | "github.com/gohugoio/hugoreleaser/internal/common/matchers" 8 | ) 9 | 10 | func TestCompilePaths(t *testing.T) { 11 | c := qt.New(t) 12 | 13 | checkMatchesEverything := func(core *Core) bool { 14 | for _, m := range []matchers.Matcher{core.PathsBuildsCompiled, core.PathsReleasesCompiled} { 15 | if !m.Match("aasdfasd32dfasdfasdf") { 16 | return false 17 | } 18 | } 19 | return true 20 | } 21 | 22 | for _, test := range []struct { 23 | name string 24 | paths []string 25 | assert func(c *qt.C, core *Core) 26 | }{ 27 | { 28 | name: "empty", 29 | paths: []string{}, 30 | assert: func(c *qt.C, core *Core) { 31 | c.Assert(checkMatchesEverything(core), qt.IsTrue) 32 | }, 33 | }, 34 | { 35 | name: "double asterisk", 36 | paths: []string{"**"}, 37 | assert: func(c *qt.C, core *Core) { 38 | c.Assert(checkMatchesEverything(core), qt.IsTrue) 39 | }, 40 | }, 41 | { 42 | name: "match some builds", 43 | paths: []string{"builds/foo*"}, 44 | assert: func(c *qt.C, core *Core) { 45 | c.Assert(core.PathsBuildsCompiled.Match("foos"), qt.IsTrue) 46 | c.Assert(core.PathsBuildsCompiled.Match("bar"), qt.IsFalse) 47 | c.Assert(core.PathsReleasesCompiled.Match("adfasdfasfd"), qt.IsTrue) 48 | 49 | }, 50 | }, 51 | { 52 | name: "match some releases", 53 | paths: []string{"releases/foo*"}, 54 | assert: func(c *qt.C, core *Core) { 55 | c.Assert(core.PathsReleasesCompiled.Match("foo"), qt.IsTrue) 56 | c.Assert(core.PathsReleasesCompiled.Match("bar"), qt.IsFalse) 57 | c.Assert(core.PathsBuildsCompiled.Match("adfasdfasfd"), qt.IsTrue) 58 | 59 | }, 60 | }, 61 | { 62 | name: "match some builds and releases", 63 | paths: []string{"releases/foo*", "builds/bar*"}, 64 | assert: func(c *qt.C, core *Core) { 65 | c.Assert(core.PathsReleasesCompiled.Match("foos"), qt.IsTrue) 66 | c.Assert(core.PathsReleasesCompiled.Match("bars"), qt.IsFalse) 67 | c.Assert(core.PathsReleasesCompiled.Match("asdfasdf"), qt.IsFalse) 68 | c.Assert(core.PathsBuildsCompiled.Match("adfasdfasfd"), qt.IsFalse) 69 | 70 | }, 71 | }, 72 | { 73 | name: "multiple release paths", 74 | paths: []string{"releases/foo*", "releases/**.zip"}, 75 | assert: func(c *qt.C, core *Core) { 76 | c.Assert(core.PathsReleasesCompiled.Match("foos.zip"), qt.IsTrue) 77 | 78 | }, 79 | }, 80 | } { 81 | c.Run(test.name, func(c *qt.C) { 82 | core := &Core{ 83 | Paths: test.paths, 84 | } 85 | c.Assert(core.compilePaths(), qt.IsNil) 86 | test.assert(c, core) 87 | }) 88 | } 89 | 90 | c.Assert((&Core{Paths: []string{"/**"}}).compilePaths(), qt.Not(qt.IsNil)) 91 | 92 | } 93 | -------------------------------------------------------------------------------- /cmd/releasecmd/release.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package releasecmd 16 | 17 | import ( 18 | "context" 19 | _ "embed" 20 | "flag" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | "text/template" 26 | 27 | "github.com/bep/logg" 28 | "github.com/gohugoio/hugoreleaser/cmd/corecmd" 29 | "github.com/gohugoio/hugoreleaser/internal/common/matchers" 30 | "github.com/gohugoio/hugoreleaser/internal/common/templ" 31 | "github.com/gohugoio/hugoreleaser/internal/config" 32 | "github.com/gohugoio/hugoreleaser/internal/releases" 33 | "github.com/gohugoio/hugoreleaser/internal/releases/changelog" 34 | "github.com/gohugoio/hugoreleaser/staticfiles" 35 | "github.com/peterbourgon/ff/v3/ffcli" 36 | ) 37 | 38 | const commandName = "release" 39 | 40 | // New returns a usable ffcli.Command for the release subcommand. 41 | func New(core *corecmd.Core) *ffcli.Command { 42 | fs := flag.NewFlagSet(corecmd.CommandName+" "+commandName, flag.ExitOnError) 43 | 44 | releaser := NewReleaser(core, fs) 45 | 46 | core.RegisterFlags(fs) 47 | 48 | return &ffcli.Command{ 49 | Name: "release", 50 | ShortUsage: corecmd.CommandName + " build [flags] ", 51 | ShortHelp: "Prepare and publish one or more releases.", 52 | FlagSet: fs, 53 | Exec: releaser.Exec, 54 | } 55 | } 56 | 57 | // NewReleaser returns a new Releaser. 58 | func NewReleaser(core *corecmd.Core, fs *flag.FlagSet) *Releaser { 59 | r := &Releaser{ 60 | core: core, 61 | } 62 | 63 | fs.StringVar(&r.commitish, "commitish", "", "The commitish value that determines where the Git tag is created from.") 64 | 65 | return r 66 | } 67 | 68 | type Releaser struct { 69 | core *corecmd.Core 70 | infoLog logg.LevelLogger 71 | 72 | // Flags 73 | commitish string 74 | } 75 | 76 | func (b *Releaser) Init() error { 77 | if b.commitish == "" { 78 | return fmt.Errorf("%s: flag -commitish is required", commandName) 79 | } 80 | 81 | b.infoLog = b.core.InfoLog.WithField("cmd", commandName) 82 | 83 | releaseMatches := b.core.Config.FindReleases(b.core.PathsReleasesCompiled) 84 | if len(releaseMatches) == 0 { 85 | return fmt.Errorf("%s: no releases found matching -paths %v", commandName, b.core.Paths) 86 | } 87 | for _, r := range releaseMatches { 88 | if err := releases.Validate(r.ReleaseSettings.TypeParsed); err != nil { 89 | return err 90 | } 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func (b *Releaser) Exec(ctx context.Context, args []string) error { 97 | if err := b.Init(); err != nil { 98 | return err 99 | } 100 | 101 | if len(b.core.Config.Releases) == 0 { 102 | return fmt.Errorf("%s: no releases defined in config", commandName) 103 | } 104 | 105 | logFields := logg.Fields{ 106 | logg.Field{ 107 | Name: "tag", Value: b.core.Tag, 108 | }, 109 | logg.Field{ 110 | Name: "commitish", Value: b.commitish, 111 | }, 112 | } 113 | 114 | if len(b.core.Paths) > 0 { 115 | logFields = append(logFields, logg.Field{Name: "paths", Value: b.core.Paths}) 116 | } 117 | 118 | logCtx := b.infoLog.WithFields(logFields) 119 | 120 | logCtx.Log(logg.String("Finding releases")) 121 | releaseMatches := b.core.Config.FindReleases(b.core.PathsReleasesCompiled) 122 | 123 | for _, release := range releaseMatches { 124 | if err := b.handleRelease(ctx, logCtx, release); err != nil { 125 | return err 126 | } 127 | 128 | } 129 | 130 | return nil 131 | } 132 | 133 | type releaseContext struct { 134 | Ctx context.Context 135 | Log logg.LevelLogger 136 | ReleaseDir string 137 | Client releases.Client 138 | Info releases.ReleaseInfo 139 | } 140 | 141 | func (b *Releaser) handleRelease(ctx context.Context, logCtx logg.LevelLogger, release config.Release) error { 142 | releaseDir := filepath.Join( 143 | b.core.DistDir, 144 | b.core.Config.Project, 145 | b.core.Tag, 146 | b.core.DistRootReleases, 147 | filepath.FromSlash(release.Path), 148 | ) 149 | 150 | info := releases.ReleaseInfo{ 151 | Project: b.core.Config.Project, 152 | Tag: b.core.Tag, 153 | Commitish: b.commitish, 154 | Settings: release.ReleaseSettings, 155 | } 156 | 157 | var client releases.Client 158 | if b.core.Try { 159 | client = &releases.FakeClient{} 160 | } else { 161 | var err error 162 | client, err = releases.NewClient(ctx, release.ReleaseSettings.TypeParsed) 163 | if err != nil { 164 | return fmt.Errorf("%s: failed to create release client: %v", commandName, err) 165 | } 166 | } 167 | 168 | rctx := releaseContext{ 169 | Ctx: ctx, 170 | Log: logCtx, 171 | ReleaseDir: releaseDir, 172 | Info: info, 173 | Client: client, 174 | } 175 | 176 | if _, err := os.Stat(rctx.ReleaseDir); err == nil || os.IsNotExist(err) { 177 | if !os.IsNotExist(err) { 178 | // Start fresh. 179 | if err := os.RemoveAll(rctx.ReleaseDir); err != nil { 180 | return fmt.Errorf("%s: failed to remove release directory %q: %s", commandName, rctx.ReleaseDir, err) 181 | } 182 | } 183 | if err := os.MkdirAll(releaseDir, 0o755); err != nil { 184 | return fmt.Errorf("%s: failed to create release directory %q: %s", commandName, rctx.ReleaseDir, err) 185 | } 186 | 187 | } 188 | 189 | // First collect all files to be released. 190 | var archiveFilenames []string 191 | 192 | for _, archPath := range release.ArchsCompiled { 193 | archiveDir := filepath.Join( 194 | b.core.DistDir, 195 | b.core.Config.Project, 196 | b.core.Tag, 197 | b.core.DistRootArchives, 198 | filepath.FromSlash(archPath.Path), 199 | ) 200 | archiveFilenames = append(archiveFilenames, filepath.Join(archiveDir, archPath.Name)) 201 | for _, alias := range archPath.Aliases { 202 | archiveFilenames = append(archiveFilenames, filepath.Join(archiveDir, alias)) 203 | } 204 | } 205 | 206 | if b.core.Try { 207 | return nil 208 | } 209 | 210 | if len(archiveFilenames) > 0 { 211 | 212 | checksumFilename, err := b.generateChecksumTxt(rctx, archiveFilenames...) 213 | if err != nil { 214 | return err 215 | } 216 | 217 | archiveFilenames = append(archiveFilenames, checksumFilename) 218 | 219 | logCtx.Logf("Prepared %d files to archive: %v", len(archiveFilenames), archiveFilenames) 220 | 221 | } 222 | 223 | // Generate release notes if needed. 224 | // Write them to the release dir in dist to make testing easier. 225 | if info.Settings.ReleaseNotesSettings.Generate { 226 | releaseNotesFilename, err := b.generateReleaseNotes(rctx) 227 | if err != nil { 228 | return err 229 | } 230 | if releaseNotesFilename == "" { 231 | panic("releaseNotesFilename is empty") 232 | } 233 | info.Settings.ReleaseNotesSettings.Filename = releaseNotesFilename 234 | } 235 | 236 | // Now create the release archive and upload files. 237 | releaseID, err := client.Release(ctx, info) 238 | if err != nil { 239 | return fmt.Errorf("%s: failed to create release: %v", commandName, err) 240 | } 241 | r, ctx := b.core.Workforce.Start(ctx) 242 | 243 | for _, archiveFilename := range archiveFilenames { 244 | archiveFilename := archiveFilename 245 | r.Run(func() error { 246 | openFile := func() (*os.File, error) { 247 | return os.Open(archiveFilename) 248 | } 249 | logCtx.Logf("Uploading release file %s", archiveFilename) 250 | if err := releases.UploadAssetsFileWithRetries(ctx, client, info, releaseID, openFile); err != nil { 251 | return err 252 | } 253 | return nil 254 | }) 255 | } 256 | 257 | if err := r.Wait(); err != nil { 258 | return fmt.Errorf("%s: failed to upload files: %v", commandName, err) 259 | } 260 | 261 | return nil 262 | } 263 | 264 | func (b *Releaser) generateReleaseNotes(rctx releaseContext) (string, error) { 265 | if rctx.Info.Settings.ReleaseNotesSettings.Filename != "" { 266 | return "", fmt.Errorf("%s: both GenerateReleaseNotes and ReleaseNotesFilename are set for release type %q", commandName, rctx.Info.Settings.Type) 267 | } 268 | 269 | var resolveUsername func(commit, author string) (string, error) 270 | if unc, ok := rctx.Client.(releases.UsernameResolver); ok { 271 | resolveUsername = func(commit, author string) (string, error) { 272 | return unc.ResolveUsername(rctx.Ctx, commit, author, rctx.Info) 273 | } 274 | } 275 | 276 | infos, err := changelog.CollectChanges( 277 | changelog.Options{ 278 | Tag: b.core.Tag, 279 | Commitish: b.commitish, 280 | RepoPath: os.Getenv("HUGORELEASER_CHANGELOG_GITREPO"), // Set in tests. 281 | ResolveUserName: resolveUsername, 282 | }, 283 | ) 284 | if err != nil { 285 | return "", err 286 | } 287 | 288 | changeGroups := rctx.Info.Settings.ReleaseNotesSettings.Groups 289 | shortThreshold := rctx.Info.Settings.ReleaseNotesSettings.ShortThreshold 290 | if shortThreshold > 0 && len(infos) < shortThreshold { 291 | shortTitle := rctx.Info.Settings.ReleaseNotesSettings.ShortTitle 292 | if shortTitle == "" { 293 | shortTitle = "What's Changed" 294 | } 295 | var changeGroupsShort []config.ReleaseNotesGroup 296 | // Preserve any ignore groups. 297 | for _, g := range changeGroups { 298 | if g.Ignore { 299 | changeGroupsShort = append(changeGroupsShort, g) 300 | } 301 | } 302 | changeGroupsShort = append(changeGroupsShort, config.ReleaseNotesGroup{Title: shortTitle, RegexpCompiled: matchers.MatchEverything}) 303 | changeGroups = changeGroupsShort 304 | } 305 | 306 | infosGrouped, err := changelog.GroupByTitleFunc(infos, func(change changelog.Change) (string, int, bool) { 307 | for i, g := range changeGroups { 308 | if g.RegexpCompiled.Match(change.Subject) { 309 | if g.Ignore { 310 | return "", 0, false 311 | } 312 | ordinal := g.Ordinal 313 | if ordinal == 0 { 314 | ordinal = i + 1 315 | } 316 | return g.Title, ordinal, true 317 | } 318 | } 319 | return "", 0, false 320 | }) 321 | 322 | if err != nil { 323 | return "", err 324 | } 325 | 326 | type ReleaseNotesContext struct { 327 | ChangeGroups []changelog.TitleChanges 328 | } 329 | 330 | rnc := ReleaseNotesContext{ 331 | ChangeGroups: infosGrouped, 332 | } 333 | 334 | releaseNotesFilename := filepath.Join(rctx.ReleaseDir, "release-notes.md") 335 | rctx.Info.Settings.ReleaseNotesSettings.Filename = releaseNotesFilename 336 | err = func() error { 337 | f, err := os.Create(releaseNotesFilename) 338 | if err != nil { 339 | return err 340 | } 341 | defer f.Close() 342 | 343 | var t *template.Template 344 | 345 | if customTemplateFilename := rctx.Info.Settings.ReleaseNotesSettings.TemplateFilename; customTemplateFilename != "" { 346 | if !filepath.IsAbs(customTemplateFilename) { 347 | customTemplateFilename = filepath.Join(b.core.ProjectDir, customTemplateFilename) 348 | } 349 | b, err := os.ReadFile(customTemplateFilename) 350 | if err != nil { 351 | return err 352 | } 353 | t, err = templ.Parse(string(b)) 354 | if err != nil { 355 | return err 356 | } 357 | } else { 358 | t = staticfiles.ReleaseNotesTemplate 359 | 360 | } 361 | 362 | if err := t.Execute(f, rnc); err != nil { 363 | return err 364 | } 365 | 366 | return nil 367 | }() 368 | 369 | if err != nil { 370 | return "", fmt.Errorf("%s: failed to create release notes file %q: %s", commandName, releaseNotesFilename, err) 371 | } 372 | 373 | rctx.Log.WithField("filename", releaseNotesFilename).Log(logg.String("Created release notes")) 374 | 375 | return releaseNotesFilename, nil 376 | } 377 | 378 | func (b *Releaser) generateChecksumTxt(rctx releaseContext, archiveFilenames ...string) (string, error) { 379 | // Create a checksums.txt file. 380 | checksumLines, err := releases.CreateChecksumLines(b.core.Workforce, archiveFilenames...) 381 | if err != nil { 382 | return "", err 383 | } 384 | // This is what Hugo got out of the box from Goreleaser. No settings for now. 385 | name := fmt.Sprintf("%s_%s_checksums.txt", rctx.Info.Project, strings.TrimPrefix(rctx.Info.Tag, "v")) 386 | 387 | checksumFilename := filepath.Join(rctx.ReleaseDir, name) 388 | err = func() error { 389 | f, err := os.Create(checksumFilename) 390 | if err != nil { 391 | return err 392 | } 393 | defer f.Close() 394 | 395 | for _, line := range checksumLines { 396 | _, err := f.WriteString(line + "\n") 397 | if err != nil { 398 | return err 399 | } 400 | } 401 | 402 | return nil 403 | }() 404 | 405 | if err != nil { 406 | return "", fmt.Errorf("%s: failed to create checksum file %q: %s", commandName, checksumFilename, err) 407 | } 408 | 409 | rctx.Log.WithField("filename", checksumFilename).Log(logg.String("Created checksum file")) 410 | 411 | return checksumFilename, nil 412 | } 413 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 0.5% 7 | patch: off 8 | 9 | comment: 10 | require_changes: true 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gohugoio/hugoreleaser 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/bep/execrpc v0.10.0 7 | github.com/bep/helpers v0.5.0 8 | github.com/bep/logg v0.4.0 9 | github.com/bep/workers v1.1.0 10 | github.com/fatih/color v1.18.0 11 | github.com/frankban/quicktest v1.14.6 12 | github.com/gohugoio/hugoreleaser/plugins v0.1.1-0.20220822083757-38d81884db04 13 | github.com/google/go-github/v45 v45.2.0 14 | github.com/mattn/go-isatty v0.0.20 15 | github.com/mitchellh/mapstructure v1.5.0 // indirect 16 | github.com/pelletier/go-toml/v2 v2.2.3 17 | github.com/peterbourgon/ff/v3 v3.4.0 18 | github.com/rogpeppe/go-internal v1.13.1 19 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 20 | golang.org/x/oauth2 v0.24.0 21 | ) 22 | 23 | require ( 24 | github.com/bep/clocks v0.5.0 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/pkg/errors v0.9.1 // indirect 27 | golang.org/x/crypto v0.31.0 // indirect 28 | golang.org/x/sys v0.28.0 // indirect 29 | ) 30 | 31 | require ( 32 | github.com/goccy/go-yaml v1.15.13-0.20241221080047-87aec7d7f886 33 | github.com/gohugoio/hugoreleaser-plugins-api v0.7.1-0.20241220094410-1f02562cf9b9 34 | golang.org/x/sync v0.10.0 35 | ) 36 | 37 | require ( 38 | github.com/google/go-querystring v1.1.0 // indirect 39 | golang.org/x/tools v0.28.0 // indirect 40 | ) 41 | 42 | require ( 43 | github.com/gobwas/glob v0.2.3 44 | github.com/google/go-cmp v0.6.0 // indirect 45 | github.com/kr/pretty v0.3.1 // indirect 46 | github.com/kr/text v0.2.0 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= 2 | github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= 3 | github.com/bep/execrpc v0.10.0 h1:Y8S8pZOFZI/zo95RMqgri98eIFaZEqZ41tbF89hSx/A= 4 | github.com/bep/execrpc v0.10.0/go.mod h1:lGHGK8NX0KvfhlRNH/SebW1quHGwXFeE3Ba+2KLtmrM= 5 | github.com/bep/helpers v0.5.0 h1:rneezhnG7GzLFlsEWO/EnleaBRuluBDGFimalO6Y50o= 6 | github.com/bep/helpers v0.5.0/go.mod h1:dSqCzIvHbzsk5YOesp1M7sKAq5xUcvANsRoKdawxH4Q= 7 | github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= 8 | github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= 9 | github.com/bep/workers v1.1.0 h1:3Xw/1y/Fzjt8KBB4nCHfXcvyWH9h56iEwPquCjKphCc= 10 | github.com/bep/workers v1.1.0/go.mod h1:7kIESOB86HfR2379pwoMWNy8B50D7r99fRLUyPSNyCs= 11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 15 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 16 | github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= 17 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 18 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 19 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 20 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 21 | github.com/goccy/go-yaml v1.15.13-0.20241221080047-87aec7d7f886 h1:DGV2XraUpNBGuKdxYuNFmlm4feqfEFMm+48p2YUQnqw= 22 | github.com/goccy/go-yaml v1.15.13-0.20241221080047-87aec7d7f886/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 23 | github.com/gohugoio/hugoreleaser-plugins-api v0.7.1-0.20241220094410-1f02562cf9b9 h1:JT6XVa3wBdkwrgcV4EYku7fZOfLX2MjWgWWtHtp5IQs= 24 | github.com/gohugoio/hugoreleaser-plugins-api v0.7.1-0.20241220094410-1f02562cf9b9/go.mod h1:Qheg3q6TF7pq9etJBNceTqGaM1VfkYDnm2k2KIIC/Ac= 25 | github.com/gohugoio/hugoreleaser/plugins v0.1.1-0.20220822083757-38d81884db04 h1:VNOiFvTuhXc2eoDvBVQHsfxl1TTS2/EF1wFs1YttIlA= 26 | github.com/gohugoio/hugoreleaser/plugins v0.1.1-0.20220822083757-38d81884db04/go.mod h1:P3JlkmIYwGFlTf8/MhkR4P+mvidrYo28Fudx4wcR7f0= 27 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 29 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 30 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 31 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 32 | github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI= 33 | github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= 34 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 35 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 36 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 37 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 38 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 39 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 40 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 41 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 42 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 43 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 44 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 45 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 46 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 47 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 48 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 49 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 50 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 51 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 52 | github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= 53 | github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= 54 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 55 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 56 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 57 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 59 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 60 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 61 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 62 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 63 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 64 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 65 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 66 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= 67 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 68 | golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= 69 | golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 70 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 72 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 73 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 76 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 77 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 78 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 79 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 82 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 83 | -------------------------------------------------------------------------------- /go.work.dev: -------------------------------------------------------------------------------- 1 | go 1.19 2 | 3 | use ( 4 | . 5 | ./maintenance 6 | 7 | ) 8 | -------------------------------------------------------------------------------- /hugoreleaser.env: -------------------------------------------------------------------------------- 1 | # Next release 2 | HUGORELEASER_TAG=v0.60.0 3 | HUGORELEASER_COMMITISH=main 4 | -------------------------------------------------------------------------------- /hugoreleaser.yaml: -------------------------------------------------------------------------------- 1 | project: hugoreleaser 2 | 3 | # definitions can be used to define anchors for common blocks and values. 4 | # But note that build_settings and archive_settings can be set on any level and will merged downwards. 5 | # Any zero config value will be replaced with the first non-zero value found above. 6 | definitions: 7 | 8 | # Useful if you have changed archive naming scheme, but want to preserve some backwards compability with the most 9 | # common variants, e.g. "linux-amd64.tar.gz: Linux-64bit.tar.gz" 10 | archive_alias_replacements: {} 11 | 12 | go_settings: 13 | go_proxy: https://proxy.golang.org 14 | go_exe: go 15 | 16 | # This can be overridden for each build, goos, or goarch if needed. 17 | build_settings: 18 | binary: hugoreleaser 19 | flags: 20 | - -buildmode 21 | - exe 22 | env: 23 | - CGO_ENABLED=0 24 | ldflags: "-s -w -X main.tag=${HUGORELEASER_TAG}" 25 | 26 | # This can be overridden for each archive. 27 | archive_settings: 28 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 29 | extra_files: 30 | - source_path: README.md 31 | target_path: README.md 32 | - source_path: LICENSE 33 | target_path: LICENSE 34 | type: 35 | format: tar.gz 36 | extension: .tar.gz 37 | release_settings: 38 | name: ${HUGORELEASER_TAG} 39 | type: github 40 | repository: hugoreleaser 41 | repository_owner: gohugoio 42 | draft: true 43 | prerelease: false 44 | release_notes_settings: 45 | generate: true 46 | generate_on_host: false 47 | filename: "" 48 | template_filename: "" 49 | short_threshold: 10 50 | short_title: What's Changed 51 | groups: 52 | - regexp: snapcraft:|Merge commit|Squashed 53 | ignore: true 54 | - title: Bug fixes 55 | regexp: fix 56 | ordinal: 20 57 | - title: Dependency Updates 58 | regexp: deps 59 | ordinal: 30 60 | - title: Documentation 61 | regexp: doc 62 | ordinal: 40 63 | - title: Improvements 64 | regexp: .* 65 | ordinal: 10 66 | builds: 67 | - path: unix 68 | os: 69 | - goos: linux 70 | archs: 71 | - goarch: amd64 72 | - path: macos 73 | os: 74 | - goos: darwin 75 | archs: 76 | - goarch: universal 77 | - path: windows 78 | os: 79 | - goos: windows 80 | build_settings: 81 | binary: hugoreleaser.exe 82 | archs: 83 | - goarch: amd64 84 | archives: 85 | - paths: 86 | - builds/unix/** 87 | - paths: 88 | - builds/macos/** 89 | archive_settings: 90 | extra_files: [] 91 | type: 92 | format: _plugin 93 | extension: .pkg 94 | plugin: 95 | id: macospkgremote 96 | type: gorun 97 | command: github.com/gohugoio/hugoreleaser-archive-plugins/macospkgremote@latest 98 | custom_settings: 99 | package_identifier: io.gohugo.hugoreleaser 100 | package_version: ${HUGORELEASER_TAG} 101 | bucket: s3fptest 102 | queue: https://sqs.eu-north-1.amazonaws.com/656975317043/s3fptest_client 103 | access_key_id: ${S3RPC_CLIENT_ACCESS_KEY_ID} 104 | secret_access_key: ${S3RPC_CLIENT_SECRET_ACCESS_KEY} 105 | - paths: 106 | - builds/**/linux/amd64 107 | archive_settings: 108 | binary_dir: /usr/local/bin 109 | extra_files: [] 110 | type: 111 | format: _plugin 112 | extension: .deb 113 | plugin: 114 | id: deb 115 | type: gorun 116 | command: github.com/gohugoio/hugoreleaser-archive-plugins/deb@latest 117 | custom_settings: 118 | vendor: gohugo.io 119 | homepage: https://github.com/gohugoio/hugoreleaser 120 | maintainer: Bjørn Erik Pedersen 121 | description: Build, archive and release Go programs. 122 | license: Apache-2.0 123 | - paths: 124 | - builds/windows/** 125 | archive_settings: 126 | type: 127 | format: zip 128 | extension: .zip 129 | releases: 130 | - paths: 131 | - archives/** 132 | path: myrelease 133 | -------------------------------------------------------------------------------- /internal/archives/archive.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package archives 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | 21 | "github.com/gohugoio/hugoreleaser/internal/archives/archiveformats" 22 | "github.com/gohugoio/hugoreleaser/internal/archives/renamer" 23 | "github.com/gohugoio/hugoreleaser/internal/archives/targz" 24 | "github.com/gohugoio/hugoreleaser/internal/archives/zip" 25 | "github.com/gohugoio/hugoreleaser/internal/common/ioh" 26 | "github.com/gohugoio/hugoreleaser/internal/config" 27 | ) 28 | 29 | func New(settings config.ArchiveSettings, out io.WriteCloser) (Archiver, error) { 30 | switch settings.Type.FormatParsed { 31 | case archiveformats.TarGz: 32 | return targz.New(out), nil 33 | case archiveformats.Zip: 34 | return zip.New(out), nil 35 | case archiveformats.Rename: 36 | return renamer.New(out), nil 37 | default: 38 | return nil, fmt.Errorf("unsupported archive format %q", settings.Type.Format) 39 | } 40 | } 41 | 42 | type Archiver interface { 43 | // AddAndClose adds a file to the archive, then closes it. 44 | AddAndClose(dir string, f ioh.File) error 45 | 46 | // Finalize finalizes the archive and closes all writers in use. 47 | // It is not safe to call AddAndClose after Finalize. 48 | Finalize() error 49 | } 50 | -------------------------------------------------------------------------------- /internal/archives/archive_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package archives 16 | 17 | import "testing" 18 | 19 | func TestArvhiceTarGz(t *testing.T) { 20 | // TODO 21 | } 22 | -------------------------------------------------------------------------------- /internal/archives/archiveformats/archiveformats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package archiveformats 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/gohugoio/hugoreleaser/internal/common/mapsh" 22 | ) 23 | 24 | // Goreleaser supports `tar.gz`, `tar.xz`, `tar`, `gz`, `zip` and `binary`. 25 | // We currently limit ourselves to what Hugo uses: `tar.gz` and 'zip' (for Windows). 26 | const ( 27 | InvalidFormat Format = iota 28 | Deb 29 | TarGz 30 | Zip 31 | Rename 32 | Plugin // Plugin is a special format that is used to indicate that the archive operation is handled by an external tool. 33 | ) 34 | 35 | var formatString = map[Format]string{ 36 | // The string values is what users can specify in the config. 37 | Deb: "deb", 38 | TarGz: "tar.gz", 39 | Zip: "zip", 40 | Rename: "rename", 41 | Plugin: "_plugin", 42 | } 43 | 44 | var stringFormat = map[string]Format{} 45 | 46 | func init() { 47 | for k, v := range formatString { 48 | stringFormat[v] = k 49 | } 50 | } 51 | 52 | // Parse parses a string into a Format. 53 | func Parse(s string) (Format, error) { 54 | f := stringFormat[strings.ToLower(s)] 55 | if f == InvalidFormat { 56 | return f, fmt.Errorf("invalid archive format %q, must be one of %s", s, mapsh.KeysSorted(formatString)) 57 | } 58 | return f, nil 59 | } 60 | 61 | // MustParse parses a string into a Format and panics if it fails. 62 | func MustParse(s string) Format { 63 | f, err := Parse(s) 64 | if err != nil { 65 | panic(err) 66 | } 67 | return f 68 | } 69 | 70 | // Format represents the type of archive. 71 | type Format int 72 | 73 | func (f Format) String() string { 74 | return formatString[f] 75 | } 76 | -------------------------------------------------------------------------------- /internal/archives/archiveformats/archiveformats_test.go: -------------------------------------------------------------------------------- 1 | package archiveformats 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | ) 8 | 9 | func TestFormat(t *testing.T) { 10 | c := qt.New(t) 11 | 12 | c.Assert(MustParse("tar.gz"), qt.Equals, TarGz) 13 | c.Assert(MustParse("tar.gz").String(), qt.Equals, "tar.gz") 14 | c.Assert(MustParse("ZiP").String(), qt.Equals, "zip") 15 | 16 | _, err := Parse("invalid") 17 | c.Assert(err, qt.ErrorMatches, "invalid archive format \"invalid\", must be one of .*") 18 | c.Assert(func() { MustParse("invalid") }, qt.PanicMatches, `invalid.*`) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /internal/archives/build.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package archives 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "os" 21 | 22 | "github.com/bep/logg" 23 | "github.com/gohugoio/hugoreleaser-plugins-api/archiveplugin" 24 | "github.com/gohugoio/hugoreleaser/cmd/corecmd" 25 | "github.com/gohugoio/hugoreleaser/internal/archives/archiveformats" 26 | "github.com/gohugoio/hugoreleaser/internal/config" 27 | ) 28 | 29 | // Build builds an archive from the given settings and writes it to req.OutFilename 30 | func Build(c *corecmd.Core, infoLogger logg.LevelLogger, settings config.ArchiveSettings, req archiveplugin.Request) (err error) { 31 | if settings.Type.FormatParsed == archiveformats.Plugin { 32 | // Delegate to external tool. 33 | return buildExternal(c, infoLogger, settings, req) 34 | } 35 | 36 | if c.Try { 37 | archive, err := New(settings, struct { 38 | io.Writer 39 | io.Closer 40 | }{ 41 | io.Discard, 42 | io.NopCloser(nil), 43 | }) 44 | if err != nil { 45 | return err 46 | } 47 | return archive.Finalize() 48 | } 49 | 50 | outFile, err := os.Create(req.OutFilename) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | archiver, err := New(settings, outFile) 56 | if err != nil { 57 | return err 58 | } 59 | defer func() { 60 | err = archiver.Finalize() 61 | }() 62 | 63 | for _, file := range req.Files { 64 | if file.Mode != 0 { 65 | if err := os.Chmod(file.SourcePathAbs, file.Mode); err != nil { 66 | return err 67 | } 68 | } 69 | 70 | f, err := os.Open(file.SourcePathAbs) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | err = archiver.AddAndClose(file.TargetPath, f) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | 81 | return 82 | } 83 | 84 | func buildExternal(c *corecmd.Core, infoLogger logg.LevelLogger, settings config.ArchiveSettings, req archiveplugin.Request) error { 85 | infoLogger = infoLogger.WithField("plugin", settings.Plugin.ID) 86 | 87 | pluginSettings := settings.Plugin 88 | 89 | client, found := c.PluginsRegistryArchive[pluginSettings.ID] 90 | if !found { 91 | return fmt.Errorf("archive plugin %q not found in registry", pluginSettings.ID) 92 | } 93 | 94 | result := client.Execute(req) 95 | if err := result.Err(); err != nil { 96 | return err 97 | } 98 | receipt := <-result.Receipt() 99 | if receipt.Error != nil { 100 | return receipt.Error 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/archives/renamer/renamer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package renamer 16 | 17 | import ( 18 | "io" 19 | "sync" 20 | 21 | "github.com/gohugoio/hugoreleaser/internal/common/ioh" 22 | ) 23 | 24 | // New returns a new Archiver for the given writer. 25 | func New(out io.WriteCloser) *Renamer { 26 | archive := &Renamer{ 27 | out: out, 28 | } 29 | return archive 30 | } 31 | 32 | // Renamer is an Archiver that just writes the first File received in AddAndClose to the underlying writer, 33 | // and drops the rest. 34 | // This construct is most useful for testing. 35 | type Renamer struct { 36 | out io.WriteCloser 37 | 38 | writeOnce sync.Once 39 | } 40 | 41 | func (a *Renamer) AddAndClose(targetPath string, f ioh.File) error { 42 | var err error 43 | a.writeOnce.Do(func() { 44 | _, err = io.Copy(a.out, f) 45 | }) 46 | return err 47 | } 48 | 49 | func (a *Renamer) Finalize() error { 50 | return a.out.Close() 51 | } 52 | -------------------------------------------------------------------------------- /internal/archives/targz/targz.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package targz 16 | 17 | import ( 18 | "archive/tar" 19 | "compress/gzip" 20 | "io" 21 | 22 | "github.com/gohugoio/hugoreleaser/internal/common/ioh" 23 | ) 24 | 25 | func New(out io.WriteCloser) *Archive { 26 | archive := &Archive{ 27 | out: out, 28 | } 29 | 30 | gw, _ := gzip.NewWriterLevel(out, gzip.BestCompression) 31 | tw := tar.NewWriter(gw) 32 | 33 | archive.gw = gw 34 | archive.tw = tw 35 | 36 | return archive 37 | } 38 | 39 | type Archive struct { 40 | out io.WriteCloser 41 | gw *gzip.Writer 42 | tw *tar.Writer 43 | } 44 | 45 | func (a *Archive) AddAndClose(targetPath string, f ioh.File) error { 46 | defer f.Close() 47 | 48 | info, err := f.Stat() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | header, err := tar.FileInfoHeader(info, "") // TODO(bep) symlink handling? 54 | if err != nil { 55 | return err 56 | } 57 | header.Name = targetPath 58 | 59 | err = a.tw.WriteHeader(header) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | _, err = io.Copy(a.tw, f) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (a *Archive) Finalize() error { 73 | if err := a.tw.Close(); err != nil { 74 | return err 75 | } 76 | if err := a.gw.Close(); err != nil { 77 | return err 78 | } 79 | 80 | return a.out.Close() 81 | } 82 | -------------------------------------------------------------------------------- /internal/archives/zip/zip.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package zip 16 | 17 | import ( 18 | "archive/zip" 19 | "io" 20 | 21 | "github.com/gohugoio/hugoreleaser/internal/common/ioh" 22 | ) 23 | 24 | func New(out io.WriteCloser) *Archive { 25 | archive := &Archive{ 26 | out: out, 27 | zipw: zip.NewWriter(out), 28 | } 29 | 30 | return archive 31 | } 32 | 33 | type Archive struct { 34 | out io.WriteCloser 35 | zipw *zip.Writer 36 | } 37 | 38 | func (a *Archive) AddAndClose(targetPath string, f ioh.File) error { 39 | defer f.Close() 40 | 41 | zw, err := a.zipw.Create(targetPath) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | _, err = io.Copy(zw, f) 47 | 48 | return err 49 | } 50 | 51 | func (a *Archive) Finalize() error { 52 | err1 := a.zipw.Close() 53 | err2 := a.out.Close() 54 | 55 | if err1 != nil { 56 | return err1 57 | } 58 | if err2 != nil { 59 | return err2 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/builds/macos_universal_binary.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package builds 16 | 17 | import ( 18 | "debug/macho" 19 | "encoding/binary" 20 | "errors" 21 | "fmt" 22 | "os" 23 | ) 24 | 25 | const ( 26 | // PSeudo Goarch used to indicate the output is a universal binary. 27 | // It's currently hardcoded to mean MacOS amd64 and arm64. 28 | UniversalGoarch = "universal" 29 | 30 | magicFat64 = macho.MagicFat + 1 31 | 32 | // Alignment wanted for each sub-file. 33 | // amd64 needs 12 bits, arm64 needs 14. We choose the max of all requirements here. 34 | alignBits = 14 35 | align = 1 << alignBits 36 | 37 | // fat64 doesn't seem to work: 38 | // - the resulting binary won't run. 39 | // - the resulting binary is parseable by lipo, but reports that the contained files are "hidden". 40 | // - the native OSX lipo can't make a fat64. 41 | fat64Supported = false 42 | ) 43 | 44 | // CreateMacOSUniversalBinary creates a universal binary for the given files. 45 | // Adapted from: https://github.com/randall77/makefat 46 | // Public domain. 47 | func CreateMacOSUniversalBinary(outputFilename string, inputFilenames ...string) error { 48 | // Read input files. 49 | type input struct { 50 | data []byte 51 | cpu uint32 52 | subcpu uint32 53 | offset int64 54 | } 55 | var inputs []input 56 | offset := int64(align) 57 | for _, inputFilename := range inputFilenames { 58 | data, err := os.ReadFile(inputFilename) 59 | if err != nil { 60 | return err 61 | } 62 | if len(data) < 12 { 63 | return fmt.Errorf("%s: too small", inputFilename) 64 | } 65 | // All currently supported mac archs (386,amd64,arm,arm64) are little endian. 66 | magic := binary.LittleEndian.Uint32(data[0:4]) 67 | if magic != macho.Magic32 && magic != macho.Magic64 { 68 | panic(fmt.Sprintf("input %s is not a macho file, magic=%x", inputFilename, magic)) 69 | } 70 | cpu := binary.LittleEndian.Uint32(data[4:8]) 71 | subcpu := binary.LittleEndian.Uint32(data[8:12]) 72 | inputs = append(inputs, input{data: data, cpu: cpu, subcpu: subcpu, offset: offset}) 73 | offset += int64(len(data)) 74 | offset = (offset + align - 1) / align * align 75 | } 76 | 77 | // Decide on whether we're doing fat32 or fat64. 78 | sixtyfour := inputs[len(inputs)-1].offset >= 1<<32 || len(inputs[len(inputs)-1].data) >= 1<<32 79 | if sixtyfour && !fat64Supported { 80 | return errors.New("files too large to fit into a fat binary") 81 | } 82 | 83 | // Make output file. 84 | out, err := os.Create(outputFilename) 85 | if err != nil { 86 | return err 87 | } 88 | err = out.Chmod(0o755) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | // Build a fat_header. 94 | var hdr []uint32 95 | if sixtyfour { 96 | hdr = append(hdr, magicFat64) 97 | } else { 98 | hdr = append(hdr, macho.MagicFat) 99 | } 100 | hdr = append(hdr, uint32(len(inputs))) 101 | 102 | // Build a fat_arch for each input file. 103 | for _, i := range inputs { 104 | hdr = append(hdr, i.cpu) 105 | hdr = append(hdr, i.subcpu) 106 | if sixtyfour { 107 | hdr = append(hdr, uint32(i.offset>>32)) // big endian 108 | } 109 | hdr = append(hdr, uint32(i.offset)) 110 | if sixtyfour { 111 | hdr = append(hdr, uint32(len(i.data)>>32)) // big endian 112 | } 113 | hdr = append(hdr, uint32(len(i.data))) 114 | hdr = append(hdr, alignBits) 115 | if sixtyfour { 116 | hdr = append(hdr, 0) // reserved 117 | } 118 | } 119 | 120 | // Write header. 121 | // Note that the fat binary header is big-endian, regardless of the 122 | // endianness of the contained files. 123 | err = binary.Write(out, binary.BigEndian, hdr) 124 | if err != nil { 125 | return err 126 | } 127 | offset = int64(4 * len(hdr)) 128 | 129 | // Write each contained file. 130 | for _, i := range inputs { 131 | if offset < i.offset { 132 | _, err = out.Write(make([]byte, i.offset-offset)) 133 | if err != nil { 134 | panic(err) 135 | } 136 | offset = i.offset 137 | } 138 | _, err := out.Write(i.data) 139 | if err != nil { 140 | panic(err) 141 | } 142 | offset += int64(len(i.data)) 143 | } 144 | return out.Close() 145 | } 146 | -------------------------------------------------------------------------------- /internal/common/errorsh/helpers.go: -------------------------------------------------------------------------------- 1 | package errorsh 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/bep/execrpc" 8 | ) 9 | 10 | // IsShutdownError returns true if the error is a shutdown error which we normally don't report to the user. 11 | func IsShutdownError(err error) bool { 12 | return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, execrpc.ErrShutdown) 13 | } 14 | -------------------------------------------------------------------------------- /internal/common/ioh/files.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ioh 16 | 17 | import ( 18 | "io" 19 | "io/fs" 20 | "os" 21 | ) 22 | 23 | type File interface { 24 | fs.File 25 | io.Writer 26 | Name() string 27 | } 28 | 29 | // RemoveAllMkdirAll is a wrapper for os.RemoveAll and os.MkdirAll. 30 | func RemoveAllMkdirAll(dirname string) error { 31 | _ = os.RemoveAll(dirname) 32 | return os.MkdirAll(dirname, 0o755) 33 | } 34 | -------------------------------------------------------------------------------- /internal/common/logging/defaulthandler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package logging contains some basic loggin setup. 16 | package logging 17 | 18 | import ( 19 | "fmt" 20 | "io" 21 | "strings" 22 | "sync" 23 | 24 | "github.com/bep/logg" 25 | 26 | "github.com/fatih/color" 27 | ) 28 | 29 | // Default Handler implementation. 30 | // Based on https://github.com/apex/log/blob/master/handlers/cli/cli.go 31 | type DefaultHandler struct { 32 | mu sync.Mutex 33 | outWriter io.Writer // Defaults to os.Stdout. 34 | errWriter io.Writer // Defaults to os.Stderr. 35 | 36 | Padding int 37 | } 38 | 39 | // NewDefaultHandler handler. 40 | func NewDefaultHandler(outWriter, errWriter io.Writer) logg.Handler { 41 | return &DefaultHandler{ 42 | outWriter: outWriter, 43 | errWriter: errWriter, 44 | Padding: 0, 45 | } 46 | } 47 | 48 | var bold = color.New(color.Bold) 49 | 50 | // Colors mapping. 51 | var Colors = [...]*color.Color{ 52 | logg.LevelDebug: color.New(color.FgWhite), 53 | logg.LevelInfo: color.New(color.FgBlue), 54 | logg.LevelWarn: color.New(color.FgYellow), 55 | logg.LevelError: color.New(color.FgRed), 56 | } 57 | 58 | // Strings mapping. 59 | var Strings = [...]string{ 60 | logg.LevelDebug: "•", 61 | logg.LevelInfo: "•", 62 | logg.LevelWarn: "•", 63 | logg.LevelError: "⨯", 64 | } 65 | 66 | // HandleLog implements logg.Handler. 67 | func (h *DefaultHandler) HandleLog(e *logg.Entry) error { 68 | color := Colors[e.Level] 69 | level := Strings[e.Level] 70 | 71 | h.mu.Lock() 72 | defer h.mu.Unlock() 73 | 74 | var w io.Writer 75 | if e.Level > logg.LevelInfo { 76 | w = h.errWriter 77 | } else { 78 | w = h.outWriter 79 | } 80 | 81 | const cmdName = "cmd" 82 | 83 | var prefix string 84 | for _, field := range e.Fields { 85 | if field.Name == cmdName { 86 | prefix = fmt.Sprint(field.Value) 87 | break 88 | } 89 | } 90 | 91 | if prefix != "" { 92 | prefix = strings.ToLower(prefix) + ": " 93 | } 94 | 95 | color.Fprintf(w, "%s %s%s", bold.Sprintf("%*s", h.Padding+1, level), color.Sprint(prefix), e.Message) 96 | 97 | for _, field := range e.Fields { 98 | if field.Name == cmdName { 99 | continue 100 | } 101 | fmt.Fprintf(w, " %s %v", color.Sprint(field.Name), field.Value) 102 | } 103 | 104 | fmt.Fprintln(w) 105 | 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /internal/common/logging/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logging 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "runtime" 21 | "strings" 22 | "time" 23 | 24 | "github.com/bep/logg" 25 | "github.com/mattn/go-isatty" 26 | ) 27 | 28 | // FormatBuildDuration formats a duration to a string on the form expected in "Total in ..." etc. 29 | func FormatBuildDuration(d time.Duration) string { 30 | if d.Milliseconds() < 2000 { 31 | return fmt.Sprintf("%dms", d.Milliseconds()) 32 | } 33 | return fmt.Sprintf("%.2fs", d.Seconds()) 34 | } 35 | 36 | // IsTerminal return true if the file descriptor is terminal and the TERM 37 | // environment variable isn't a dumb one. 38 | func IsTerminal(f *os.File) bool { 39 | if runtime.GOOS == "windows" { 40 | return false 41 | } 42 | if os.Getenv("CI") != "" { 43 | return true 44 | } 45 | 46 | fd := f.Fd() 47 | return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)) 48 | } 49 | 50 | // Replacer creates a new log handler that does string replacement in log messages. 51 | func Replacer(repl *strings.Replacer) logg.Handler { 52 | return logg.HandlerFunc(func(e *logg.Entry) error { 53 | e.Message = repl.Replace(e.Message) 54 | for i, field := range e.Fields { 55 | if s, ok := field.Value.(string); ok { 56 | e.Fields[i].Value = repl.Replace(s) 57 | } 58 | } 59 | return nil 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /internal/common/logging/nocolourshandler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logging 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "strings" 21 | "sync" 22 | 23 | "github.com/bep/logg" 24 | ) 25 | 26 | type NoColoursHandler struct { 27 | mu sync.Mutex 28 | outWriter io.Writer // Defaults to os.Stdout. 29 | errWriter io.Writer // Defaults to os.Stderr. 30 | } 31 | 32 | // NewNoColoursHandler creates a new NoColoursHandler 33 | func NewNoColoursHandler(outWriter, errWriter io.Writer) *NoColoursHandler { 34 | return &NoColoursHandler{ 35 | outWriter: outWriter, 36 | errWriter: errWriter, 37 | } 38 | } 39 | 40 | func (h *NoColoursHandler) HandleLog(e *logg.Entry) error { 41 | h.mu.Lock() 42 | defer h.mu.Unlock() 43 | 44 | var w io.Writer 45 | if e.Level > logg.LevelInfo { 46 | w = h.errWriter 47 | } else { 48 | w = h.outWriter 49 | } 50 | 51 | const cmdName = "cmd" 52 | 53 | var prefix string 54 | for _, field := range e.Fields { 55 | if field.Name == cmdName { 56 | prefix = fmt.Sprint(field.Value) 57 | break 58 | } 59 | } 60 | 61 | if prefix != "" { 62 | prefix = strings.ToUpper(prefix) + ":\t" 63 | } 64 | 65 | fmt.Fprintf(w, "%s%s", prefix, e.Message) 66 | for _, field := range e.Fields { 67 | if field.Name == cmdName { 68 | continue 69 | } 70 | fmt.Fprintf(w, " %s %q", field.Name, field.Value) 71 | } 72 | fmt.Fprintln(w) 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/common/mapsh/maps.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mapsh 16 | 17 | import ( 18 | "golang.org/x/exp/constraints" 19 | "golang.org/x/exp/slices" 20 | ) 21 | 22 | // KeysSorted returns the keys of the map sorted. 23 | func KeysSorted[M ~map[K]V, K constraints.Ordered, V any](m M) []K { 24 | keys := KeysComparable(m) 25 | slices.Sort(keys) 26 | return keys 27 | } 28 | 29 | // KeysComparable returns the keys of the map m. 30 | // The keys will be in an indeterminate order but K needs to be ordered. 31 | func KeysComparable[M ~map[K]V, K constraints.Ordered, V any](m M) []K { 32 | r := make([]K, 0, len(m)) 33 | for k := range m { 34 | r = append(r, k) 35 | } 36 | return r 37 | } 38 | -------------------------------------------------------------------------------- /internal/common/matchers/glob.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/gobwas/glob" 9 | ) 10 | 11 | type globCacheMap struct { 12 | sync.RWMutex 13 | globs map[string]Matcher 14 | } 15 | 16 | var globCache = globCacheMap{ 17 | globs: make(map[string]Matcher), 18 | } 19 | 20 | // Glob returns a matcher that matches if all given glob patterns matches the given string. 21 | // A pattern can be negated with a leading !. 22 | func Glob(patterns ...string) (Matcher, error) { 23 | if len(patterns) == 0 { 24 | return nil, errors.New("empty patterns") 25 | } 26 | if len(patterns) == 1 { 27 | return globOne(patterns[0]) 28 | } 29 | 30 | matchers := make([]Matcher, len(patterns)) 31 | for i, p := range patterns { 32 | g, err := globOne(p) 33 | if err != nil { 34 | return nil, err 35 | } 36 | matchers[i] = g 37 | } 38 | 39 | return And(matchers...), nil 40 | } 41 | 42 | func globOne(pattern string) (Matcher, error) { 43 | if pattern == "" { 44 | return nil, errors.New("empty pattern") 45 | } 46 | globCache.RLock() 47 | g, ok := globCache.globs[pattern] 48 | globCache.RUnlock() 49 | if ok { 50 | return g, nil 51 | } 52 | 53 | var negate bool 54 | if pattern[0] == '!' { 55 | negate = true 56 | pattern = strings.TrimPrefix(pattern, "!") 57 | } 58 | 59 | g, err := glob.Compile(pattern) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | if negate { 65 | g = Not(g) 66 | } 67 | 68 | globCache.Lock() 69 | globCache.globs[pattern] = g 70 | globCache.Unlock() 71 | 72 | return g, nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/common/matchers/glob_test.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | ) 8 | 9 | func TestGlob(t *testing.T) { 10 | c := qt.New(t) 11 | 12 | mustGlob := func(s ...string) Matcher { 13 | g, err := Glob(s...) 14 | c.Assert(err, qt.IsNil) 15 | c.Assert(g, qt.Not(qt.IsNil)) 16 | return g 17 | } 18 | 19 | c.Assert(mustGlob("**").Match("a"), qt.Equals, true) 20 | c.Assert(mustGlob("foo").Match("foo"), qt.Equals, true) 21 | c.Assert(mustGlob("!foo").Match("foo"), qt.Equals, false) 22 | c.Assert(mustGlob("foo", "bar").Match("foo"), qt.Equals, false) 23 | 24 | c.Assert(mustGlob("builds/**").Match("builds/abc/def"), qt.Equals, true) 25 | 26 | _, err := Glob("") 27 | c.Assert(err, qt.ErrorMatches, "empty pattern") 28 | } 29 | -------------------------------------------------------------------------------- /internal/common/matchers/matchers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package matchers 16 | 17 | type Matcher interface { 18 | Match(string) bool 19 | } 20 | 21 | func And(matchers ...Matcher) Matcher { 22 | return and(matchers) 23 | } 24 | 25 | type and []Matcher 26 | 27 | func (m and) Match(s string) bool { 28 | for _, matcher := range m { 29 | if !matcher.Match(s) { 30 | return false 31 | } 32 | } 33 | return true 34 | } 35 | 36 | type or []Matcher 37 | 38 | // Or returns a matcher that matches if any of the given matchers match. 39 | func Or(matchers ...Matcher) Matcher { 40 | return or(matchers) 41 | } 42 | 43 | func (m or) Match(s string) bool { 44 | for _, matcher := range m { 45 | if matcher.Match(s) { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | 52 | type not struct { 53 | m Matcher 54 | } 55 | 56 | // Not returns a matcher that matches if the given matcher does not match. 57 | func Not(matcher Matcher) Matcher { 58 | return not{m: matcher} 59 | } 60 | 61 | func (m not) Match(s string) bool { 62 | return !m.m.Match(s) 63 | } 64 | 65 | type MatcherFunc func(string) bool 66 | 67 | func (m MatcherFunc) Match(s string) bool { 68 | return m(s) 69 | } 70 | 71 | // MatchEverything returns a matcher that matches everything. 72 | var MatchEverything Matcher = MatcherFunc(func(s string) bool { 73 | return true 74 | }) 75 | -------------------------------------------------------------------------------- /internal/common/matchers/matchers_test.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | ) 8 | 9 | type stringMatcher string 10 | 11 | func (m stringMatcher) Match(s string) bool { 12 | return s == string(m) 13 | } 14 | 15 | func TestOps(t *testing.T) { 16 | c := qt.New(t) 17 | 18 | c.Assert(And(stringMatcher("a"), stringMatcher("b")).Match("a"), qt.Equals, false) 19 | c.Assert(And(stringMatcher("a"), stringMatcher("a")).Match("a"), qt.Equals, true) 20 | c.Assert(And(stringMatcher("a"), stringMatcher("a"), stringMatcher("a")).Match("a"), qt.Equals, true) 21 | 22 | c.Assert(Or(stringMatcher("a"), stringMatcher("b")).Match("a"), qt.Equals, true) 23 | c.Assert(Or(stringMatcher("a"), stringMatcher("b")).Match("c"), qt.Equals, false) 24 | 25 | c.Assert(Not(stringMatcher("a")).Match("a"), qt.Equals, false) 26 | c.Assert(Not(stringMatcher("a")).Match("b"), qt.Equals, true) 27 | 28 | c.Assert(MatchEverything.Match("a"), qt.Equals, true) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /internal/common/templ/templ.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package templ 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "reflect" 21 | "strings" 22 | "text/template" 23 | ) 24 | 25 | // We add a limited set of useful funcs, mostly string handling, to the Go built-ins. 26 | var BuiltInFuncs = template.FuncMap{ 27 | "upper": func(s string) string { 28 | return strings.ToUpper(s) 29 | }, 30 | "lower": func(s string) string { 31 | return strings.ToLower(s) 32 | }, 33 | "replace": strings.ReplaceAll, 34 | "trimPrefix": func(prefix, s string) string { 35 | return strings.TrimPrefix(s, prefix) 36 | }, 37 | "trimSuffix": func(suffix, s string) string { 38 | return strings.TrimSuffix(s, suffix) 39 | }, 40 | } 41 | 42 | // Sprintt renders the Go template t with the given data in ctx. 43 | func Sprintt(t string, ctx any) (string, error) { 44 | tmpl := template.New("").Funcs(BuiltInFuncs) 45 | var err error 46 | tmpl, err = tmpl.Parse(t) 47 | if err != nil { 48 | return "", err 49 | } 50 | var buf bytes.Buffer 51 | err = tmpl.Execute(&buf, ctx) 52 | if err != nil { 53 | return "", fmt.Errorf("error executing template: %v; available fields: %v", err, fieldsFromObject(ctx)) 54 | } 55 | return buf.String(), nil 56 | } 57 | 58 | // Parse parses the Go template in s. 59 | func Parse(s string) (*template.Template, error) { 60 | tmpl := template.New("").Funcs(BuiltInFuncs) 61 | var err error 62 | tmpl, err = tmpl.Parse(s) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return tmpl, nil 67 | } 68 | 69 | // MustSprintt is like Sprintt but panics on error. 70 | func MustSprintt(t string, ctx any) string { 71 | s, err := Sprintt(t, ctx) 72 | if err != nil { 73 | panic(err) 74 | } 75 | return s 76 | } 77 | 78 | func fieldsFromObject(in any) []string { 79 | var fields []string 80 | 81 | if in == nil { 82 | return fields 83 | } 84 | 85 | v := reflect.ValueOf(in) 86 | if v.Kind() != reflect.Struct { 87 | return fields 88 | } 89 | t := v.Type() 90 | for i := 0; i < v.NumField(); i++ { 91 | fields = append(fields, "."+t.Field(i).Name) 92 | } 93 | return fields 94 | 95 | } 96 | -------------------------------------------------------------------------------- /internal/common/templ/templ_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package templ 16 | 17 | import ( 18 | "testing" 19 | 20 | qt "github.com/frankban/quicktest" 21 | ) 22 | 23 | func TestSprintt(t *testing.T) { 24 | c := qt.New(t) 25 | 26 | c.Assert(MustSprintt("{{ . }}", "foo"), qt.Equals, "foo") 27 | c.Assert(MustSprintt("{{ . | upper }}", "foo"), qt.Equals, "FOO") 28 | c.Assert(MustSprintt("{{ . | lower }}", "FoO"), qt.Equals, "foo") 29 | c.Assert(MustSprintt("{{ . | trimPrefix `v` }}", "v3.0.0"), qt.Equals, "3.0.0") 30 | c.Assert(MustSprintt("{{ . | trimSuffix `-beta` }}", "v3.0.0-beta"), qt.Equals, "v3.0.0") 31 | } 32 | -------------------------------------------------------------------------------- /internal/config/archive_config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/gohugoio/hugoreleaser/internal/archives/archiveformats" 22 | "github.com/gohugoio/hugoreleaser/internal/common/matchers" 23 | "github.com/gohugoio/hugoreleaser/plugins/model" 24 | ) 25 | 26 | var ( 27 | _ model.Initializer = (*Archive)(nil) 28 | _ model.Initializer = (*ArchiveSettings)(nil) 29 | _ model.Initializer = (*ArchiveType)(nil) 30 | ) 31 | 32 | type Archive struct { 33 | // Glob of Build paths to archive. Multiple paths will be ANDed. 34 | Paths []string `json:"paths"` 35 | ArchiveSettings ArchiveSettings `json:"archive_settings"` 36 | 37 | PathsCompiled matchers.Matcher `json:"-"` 38 | ArchsCompiled []BuildArchPath `json:"-"` 39 | } 40 | 41 | func (a *Archive) Init() error { 42 | what := fmt.Sprintf("archives: %v", a.Paths) 43 | 44 | const prefix = "builds/" 45 | for i, p := range a.Paths { 46 | if !strings.HasPrefix(p, prefix) { 47 | return fmt.Errorf("%s: archive paths must start with %s", what, prefix) 48 | } 49 | a.Paths[i] = p[len(prefix):] 50 | } 51 | 52 | var err error 53 | a.PathsCompiled, err = matchers.Glob(a.Paths...) 54 | if err != nil { 55 | return fmt.Errorf("failed to compile archive paths glob %q: %v", a.Paths, err) 56 | } 57 | 58 | if err := a.ArchiveSettings.Init(); err != nil { 59 | return fmt.Errorf("%s: %v", what, err) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | type BuildArchPath struct { 66 | Arch BuildArch `json:"arch"` 67 | Path string `json:"path"` 68 | 69 | // Name is the name of the archive with the extension. 70 | Name string `json:"name"` 71 | 72 | // Any archive aliase names, with the extension. 73 | Aliases []string `json:"aliases"` 74 | } 75 | 76 | type ArchiveSettings struct { 77 | Type ArchiveType `json:"type"` 78 | 79 | BinaryDir string `json:"binary_dir"` 80 | NameTemplate string `json:"name_template"` 81 | ExtraFiles []ArchiveFileInfo `json:"extra_files"` 82 | Replacements map[string]string `json:"replacements"` 83 | Plugin Plugin `json:"plugin"` 84 | 85 | // CustomSettings is archive type specific metadata. 86 | // See in the documentation for the configured archive type. 87 | CustomSettings map[string]any `json:"custom_settings"` 88 | 89 | ReplacementsCompiled *strings.Replacer `json:"-"` 90 | } 91 | 92 | func (a *ArchiveSettings) Init() error { 93 | what := "archive_settings" 94 | 95 | if err := a.Type.Init(); err != nil { 96 | return fmt.Errorf("%s: %v", what, err) 97 | } 98 | 99 | // Validate format setup. 100 | switch a.Type.FormatParsed { 101 | case archiveformats.Plugin: 102 | if err := a.Plugin.Init(); err != nil { 103 | return fmt.Errorf("%s: %v", what, err) 104 | } 105 | default: 106 | // Clear it to we don't need to start it. 107 | a.Plugin.Clear() 108 | 109 | } 110 | 111 | var oldNew []string 112 | for k, v := range a.Replacements { 113 | oldNew = append(oldNew, k, v) 114 | } 115 | 116 | a.ReplacementsCompiled = strings.NewReplacer(oldNew...) 117 | 118 | return nil 119 | } 120 | 121 | type ArchiveType struct { 122 | Format string `json:"format"` 123 | Extension string `json:"extension"` 124 | 125 | FormatParsed archiveformats.Format `json:"-"` 126 | } 127 | 128 | func (a *ArchiveType) Init() error { 129 | what := "type" 130 | if a.Format == "" { 131 | return fmt.Errorf("%s: has no format", what) 132 | } 133 | if a.Extension == "" { 134 | return fmt.Errorf("%s: has no extension", what) 135 | } 136 | var err error 137 | if a.FormatParsed, err = archiveformats.Parse(a.Format); err != nil { 138 | return err 139 | } 140 | 141 | return nil 142 | } 143 | 144 | // IsZero is needed to get the shallow merge correct. 145 | func (a ArchiveType) IsZero() bool { 146 | return a.Format == "" && a.Extension == "" 147 | } 148 | 149 | type Archives []Archive 150 | -------------------------------------------------------------------------------- /internal/config/build_config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "fmt" 19 | "path" 20 | 21 | "github.com/bep/logg" 22 | "github.com/gohugoio/hugoreleaser/internal/builds" 23 | "github.com/gohugoio/hugoreleaser/plugins/model" 24 | ) 25 | 26 | var _ model.Initializer = (*Build)(nil) 27 | 28 | type Build struct { 29 | Path string `json:"path"` 30 | Os []BuildOs `json:"os"` 31 | 32 | BuildSettings BuildSettings `json:"build_settings"` 33 | } 34 | 35 | func (b *Build) Init() error { 36 | for _, os := range b.Os { 37 | for _, arch := range os.Archs { 38 | if arch.Goarch == builds.UniversalGoarch && os.Goos != "darwin" { 39 | return fmt.Errorf("universal arch is only supported on MacOS (GOOS=darwin)") 40 | } 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func (b Build) IsZero() bool { 47 | return b.Path == "" && len(b.Os) == 0 48 | } 49 | 50 | var _ logg.Fielder = BuildSettings{} 51 | 52 | type BuildSettings struct { 53 | Binary string `json:"binary"` 54 | 55 | Env []string `json:"env"` 56 | Ldflags string `json:"ldflags"` 57 | Flags []string `json:"flags"` 58 | 59 | GoSettings GoSettings `json:"go_settings"` 60 | } 61 | 62 | // Fields is used by the logging framework. 63 | func (b BuildSettings) Fields() logg.Fields { 64 | return logg.Fields{ 65 | logg.Field{Name: "flags", Value: b.Flags}, 66 | logg.Field{Name: "ldflags", Value: b.Ldflags}, 67 | } 68 | } 69 | 70 | type GoSettings struct { 71 | GoExe string `json:"go_exe"` 72 | GoProxy string `json:"go_proxy"` 73 | } 74 | 75 | type Builds []Build 76 | 77 | type BuildArch struct { 78 | Goarch string `json:"goarch"` 79 | 80 | BuildSettings BuildSettings `json:"build_settings"` 81 | 82 | // Tree navigation. 83 | Build *Build `json:"-"` 84 | Os *BuildOs `json:"-"` 85 | } 86 | 87 | // BinaryPath returns the path to the built binary starting below /builds. 88 | func (b BuildArch) BinaryPath() string { 89 | return path.Join(b.Build.Path, b.Os.Goos, b.Goarch, b.BuildSettings.Binary) 90 | } 91 | 92 | type BuildOs struct { 93 | Goos string `json:"goos"` 94 | Archs []BuildArch `json:"archs"` 95 | 96 | BuildSettings BuildSettings `json:"build_settings"` 97 | 98 | // Tree navigation. 99 | Build *Build `json:"-"` 100 | } 101 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "fmt" 19 | "io/fs" 20 | 21 | "github.com/gohugoio/hugoreleaser/internal/common/matchers" 22 | "github.com/gohugoio/hugoreleaser/internal/plugins/plugintypes" 23 | ) 24 | 25 | type Config struct { 26 | // A bucket for anchors that defines reusable YAML fragments. 27 | Definitions map[string]any ` json:"definitions"` 28 | 29 | Project string `json:"project"` 30 | ArchiveAliasReplacements map[string]string `json:"archive_alias_replacements"` 31 | 32 | GoSettings GoSettings `json:"go_settings"` 33 | 34 | Builds Builds `json:"builds"` 35 | Archives Archives `json:"archives"` 36 | Releases Releases `json:"releases"` 37 | 38 | BuildSettings BuildSettings `json:"build_settings"` 39 | ArchiveSettings ArchiveSettings `json:"archive_settings"` 40 | ReleaseSettings ReleaseSettings `json:"release_settings"` 41 | } 42 | 43 | func (c Config) FindReleases(filter matchers.Matcher) []Release { 44 | var releases []Release 45 | for _, release := range c.Releases { 46 | if filter == nil || filter.Match(release.Path) { 47 | releases = append(releases, release) 48 | } 49 | } 50 | return releases 51 | } 52 | 53 | // FindArchs returns the archs that match the given filter 54 | func (c Config) FindArchs(filter matchers.Matcher) []BuildArchPath { 55 | var archs []BuildArchPath 56 | for _, build := range c.Builds { 57 | buildPath := build.Path 58 | for _, os := range build.Os { 59 | osPath := buildPath + "/" + os.Goos 60 | for _, arch := range os.Archs { 61 | archPath := osPath + "/" + arch.Goarch 62 | if filter.Match(archPath) { 63 | archs = append(archs, BuildArchPath{Arch: arch, Path: archPath}) 64 | } 65 | } 66 | } 67 | } 68 | return archs 69 | } 70 | 71 | type Plugin struct { 72 | ID string `json:"id"` 73 | Type string `json:"type"` 74 | Command string `json:"command"` 75 | Dir string `json:"dir"` 76 | Env []string `json:"env"` 77 | 78 | TypeParsed plugintypes.Type `json:"-"` 79 | } 80 | 81 | func (t *Plugin) Clear() { 82 | t.ID = "" 83 | t.Type = "" 84 | t.Command = "" 85 | t.Dir = "" 86 | t.TypeParsed = plugintypes.Invalid 87 | } 88 | 89 | func (t *Plugin) Init() error { 90 | what := "plugin" 91 | if t.ID == "" { 92 | return fmt.Errorf("%s: has no id", what) 93 | } 94 | if t.Command == "" { 95 | return fmt.Errorf("%s: %q has no command", what, t.ID) 96 | } 97 | 98 | var err error 99 | if t.TypeParsed, err = plugintypes.Parse(t.Type); err != nil { 100 | return fmt.Errorf("%s: %v", what, err) 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func (t Plugin) IsZero() bool { 107 | return t.ID == "" 108 | } 109 | 110 | type ArchiveFileInfo struct { 111 | SourcePath string `json:"source_path"` 112 | TargetPath string `json:"target_path"` 113 | Mode fs.FileMode `json:"mode"` 114 | } 115 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strings" 21 | "testing" 22 | 23 | qt "github.com/frankban/quicktest" 24 | ) 25 | 26 | func TestDecode(t *testing.T) { 27 | c := qt.New(t) 28 | 29 | c.Run("Invalid archive format", func(c *qt.C) { 30 | file := ` 31 | [[archives]] 32 | [archives.archive_settings] 33 | format = "foo" 34 | ` 35 | 36 | _, err := DecodeAndApplyDefaults(strings.NewReader(file)) 37 | c.Assert(err, qt.Not(qt.IsNil)) 38 | }) 39 | } 40 | 41 | func TestDecodeFile(t *testing.T) { 42 | c := qt.New(t) 43 | 44 | f, err := os.Open("../../hugoreleaser.yaml") 45 | c.Assert(err, qt.IsNil) 46 | defer f.Close() 47 | 48 | cfg, err := DecodeAndApplyDefaults(f) 49 | if err != nil { 50 | fmt.Printf("%v\n", err) 51 | } 52 | 53 | c.Assert(err, qt.IsNil) 54 | c.Assert(cfg.Project, qt.Equals, "hugoreleaser") 55 | 56 | assertHasBuildSettings := func(b BuildSettings) { 57 | c.Helper() 58 | c.Assert(b.Env, qt.IsNotNil) 59 | c.Assert(b.Flags, qt.IsNotNil) 60 | c.Assert(b.GoSettings.GoProxy, qt.Not(qt.Equals), "") 61 | c.Assert(b.GoSettings.GoExe, qt.Not(qt.Equals), "") 62 | } 63 | 64 | assertHasBuildSettings(cfg.BuildSettings) 65 | for _, b := range cfg.Builds { 66 | assertHasBuildSettings(b.BuildSettings) 67 | for _, o := range b.Os { 68 | assertHasBuildSettings(o.BuildSettings) 69 | for _, a := range o.Archs { 70 | assertHasBuildSettings(a.BuildSettings) 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/config/decode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "bufio" 19 | "io" 20 | "os" 21 | "reflect" 22 | "strings" 23 | 24 | "github.com/goccy/go-yaml" 25 | 26 | "github.com/bep/helpers/envhelpers" 27 | ) 28 | 29 | var zeroType = reflect.TypeOf((*zeroer)(nil)).Elem() 30 | 31 | // DecodeAndApplyDefaults first expand any environment variables in r (${var}), 32 | // decodes it and applies default values. 33 | func DecodeAndApplyDefaults(r io.Reader) (Config, error) { 34 | cfg := &Config{} 35 | 36 | // Expand environment variables in the source. 37 | // This is not the most effective, but it's certianly very simple. 38 | // And the config files should be fairly small. 39 | var buf strings.Builder 40 | _, err := io.Copy(&buf, r) 41 | if err != nil { 42 | return *cfg, err 43 | } 44 | s := buf.String() 45 | 46 | s = envhelpers.Expand(s, func(k string) string { 47 | return os.Getenv(k) 48 | }) 49 | 50 | d := yaml.NewDecoder(strings.NewReader(s), 51 | yaml.DisallowUnknownField(), 52 | ) 53 | 54 | err = d.Decode(cfg) 55 | if err != nil { 56 | return *cfg, err 57 | } 58 | 59 | // Apply defaults. 60 | if cfg.GoSettings.GoExe == "" { 61 | cfg.GoSettings.GoExe = "go" 62 | } 63 | 64 | if cfg.GoSettings.GoProxy == "" { 65 | cfg.GoSettings.GoProxy = "https://proxy.golang.org" 66 | } 67 | 68 | // Merge build settings. 69 | // We may have build settings on any of Project > Build > Goos > Goarch. 70 | // Note that this uses the replaces any zero value as defined by IsTruthfulValue (a Hugo construct)m 71 | // meaning any value on the right will be used if the left is zero according to that definition. 72 | shallowMerge(&cfg.BuildSettings.GoSettings, cfg.GoSettings) 73 | for i := range cfg.Builds { 74 | shallowMerge(&cfg.Builds[i].BuildSettings, cfg.BuildSettings) 75 | shallowMerge(&cfg.Builds[i].BuildSettings.GoSettings, cfg.BuildSettings.GoSettings) 76 | 77 | for j := range cfg.Builds[i].Os { 78 | shallowMerge(&cfg.Builds[i].Os[j].BuildSettings, cfg.Builds[i].BuildSettings) 79 | shallowMerge(&cfg.Builds[i].Os[j].BuildSettings.GoSettings, cfg.Builds[i].BuildSettings.GoSettings) 80 | 81 | for k := range cfg.Builds[i].Os[j].Archs { 82 | shallowMerge(&cfg.Builds[i].Os[j].Archs[k].BuildSettings, cfg.Builds[i].Os[j].BuildSettings) 83 | shallowMerge(&cfg.Builds[i].Os[j].Archs[k].BuildSettings.GoSettings, cfg.Builds[i].Os[j].BuildSettings.GoSettings) 84 | 85 | } 86 | } 87 | } 88 | 89 | // Merge archive settings. 90 | // We may have archive settings on all of Project > Archive. 91 | for i := range cfg.Archives { 92 | shallowMerge(&cfg.Archives[i].ArchiveSettings, cfg.ArchiveSettings) 93 | } 94 | 95 | // Merge release settings. 96 | // We may have release settings on all of Project > Release. 97 | for i := range cfg.Releases { 98 | shallowMerge(&cfg.Releases[i].ReleaseSettings, cfg.ReleaseSettings) 99 | shallowMerge(&cfg.Releases[i].ReleaseSettings.ReleaseNotesSettings, cfg.ReleaseSettings.ReleaseNotesSettings) 100 | } 101 | 102 | // Init and validate build settings. 103 | for i := range cfg.Builds { 104 | if err := cfg.Builds[i].Init(); err != nil { 105 | return *cfg, err 106 | } 107 | } 108 | 109 | // Init and validate archive configs. 110 | for i := range cfg.Archives { 111 | if err := cfg.Archives[i].Init(); err != nil { 112 | return *cfg, err 113 | } 114 | } 115 | 116 | // Init and validate release configs. 117 | for i := range cfg.Releases { 118 | if err := cfg.Releases[i].Init(); err != nil { 119 | return *cfg, err 120 | } 121 | } 122 | 123 | // Apply some convenient navigation helpers. 124 | for i := range cfg.Builds { 125 | for j := range cfg.Builds[i].Os { 126 | cfg.Builds[i].Os[j].Build = &cfg.Builds[i] 127 | for k := range cfg.Builds[i].Os[j].Archs { 128 | cfg.Builds[i].Os[j].Archs[k].Build = &cfg.Builds[i] 129 | cfg.Builds[i].Os[j].Archs[k].Os = &cfg.Builds[i].Os[j] 130 | } 131 | } 132 | } 133 | 134 | return *cfg, nil 135 | } 136 | 137 | // LoadEnvFile loads environment variables from text file on the form key=value. 138 | // It ignores empty lines and lines starting with # and lines without an equals sign. 139 | func LoadEnvFile(filename string) (map[string]string, error) { 140 | fi, err := os.Stat(filename) 141 | if err != nil || fi.IsDir() { 142 | return nil, nil 143 | } 144 | 145 | f, err := os.Open(filename) 146 | if err != nil { 147 | return nil, err 148 | } 149 | defer f.Close() 150 | 151 | env := make(map[string]string) 152 | scanner := bufio.NewScanner(f) 153 | for scanner.Scan() { 154 | line := strings.TrimSpace(scanner.Text()) 155 | if line == "" || strings.HasPrefix(line, "#") { 156 | continue 157 | } 158 | key, value, found := strings.Cut(line, "=") 159 | if !found { 160 | continue 161 | } 162 | env[strings.TrimSpace(key)] = strings.TrimSpace(value) 163 | } 164 | return env, scanner.Err() 165 | } 166 | 167 | type zeroer interface { 168 | IsZero() bool 169 | } 170 | 171 | // This is based on "thruthy" function used in the Hugo template system, reused for a slightly different domain. 172 | // The only difference is that this function returns true for empty non-nil slices and maps. 173 | // 174 | // isTruthfulValue returns whether the given value has a meaningful truth value. 175 | // This is based on template.IsTrue in Go's stdlib, but also considers 176 | // IsZero and any interface value will be unwrapped before it's considered 177 | // for truthfulness. 178 | // 179 | // Based on: 180 | // https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306 181 | func isTruthfulValue(val reflect.Value) (truth bool) { 182 | val = indirectInterface(val) 183 | 184 | if !val.IsValid() { 185 | // Something like var x interface{}, never set. It's a form of nil. 186 | return 187 | } 188 | 189 | if val.Type().Implements(zeroType) { 190 | return !val.Interface().(zeroer).IsZero() 191 | } 192 | 193 | switch val.Kind() { 194 | case reflect.Array, reflect.Map, reflect.Slice: 195 | return !val.IsNil() 196 | case reflect.String: 197 | truth = val.Len() > 0 198 | case reflect.Bool: 199 | truth = val.Bool() 200 | case reflect.Complex64, reflect.Complex128: 201 | truth = val.Complex() != 0 202 | case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: 203 | truth = !val.IsNil() 204 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 205 | truth = val.Int() != 0 206 | case reflect.Float32, reflect.Float64: 207 | truth = val.Float() != 0 208 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 209 | truth = val.Uint() != 0 210 | case reflect.Struct: 211 | truth = true // Struct values are always true. 212 | default: 213 | return 214 | } 215 | 216 | return 217 | } 218 | 219 | // Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931 220 | func indirectInterface(v reflect.Value) reflect.Value { 221 | if v.Kind() != reflect.Interface { 222 | return v 223 | } 224 | if v.IsNil() { 225 | return reflect.Value{} 226 | } 227 | return v.Elem() 228 | } 229 | 230 | func shallowMerge(dst, src any) { 231 | dstv := reflect.ValueOf(dst) 232 | if dstv.Kind() != reflect.Ptr { 233 | panic("dst is not a pointer") 234 | } 235 | 236 | dstv = reflect.Indirect(dstv) 237 | srcv := reflect.Indirect(reflect.ValueOf(src)) 238 | 239 | for i := 0; i < dstv.NumField(); i++ { 240 | v := dstv.Field(i) 241 | if !v.CanSet() { 242 | continue 243 | } 244 | if !isTruthfulValue(v) { 245 | v.Set(srcv.Field(i)) 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /internal/config/release_config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "fmt" 19 | "path" 20 | "path/filepath" 21 | "regexp" 22 | "strings" 23 | 24 | "github.com/gohugoio/hugoreleaser/internal/common/matchers" 25 | "github.com/gohugoio/hugoreleaser/internal/releases/releasetypes" 26 | ) 27 | 28 | type Release struct { 29 | // Paths with Glob of releases paths to release. Multiple paths will be ANDed. 30 | Paths []string `json:"paths"` 31 | 32 | // Path is the directory below /dist/releases where the release artifacts gets stored. 33 | // This must be unique for each release within one configuration file. 34 | Path string `json:"path"` 35 | 36 | ReleaseSettings ReleaseSettings `json:"release_settings"` 37 | 38 | PathsCompiled matchers.Matcher `json:"-"` 39 | 40 | // Builds matching Paths. 41 | ArchsCompiled []BuildArchPath `json:"-"` 42 | } 43 | 44 | func (a *Release) Init() error { 45 | what := "releases" 46 | 47 | if a.Path == "" { 48 | return fmt.Errorf("%s: dir is required", what) 49 | } 50 | 51 | a.Path = path.Clean(filepath.ToSlash(a.Path)) 52 | 53 | const prefix = "archives/" 54 | for i, p := range a.Paths { 55 | if !strings.HasPrefix(p, prefix) { 56 | return fmt.Errorf("%s: archive paths must start with %s", what, prefix) 57 | } 58 | a.Paths[i] = p[len(prefix):] 59 | } 60 | 61 | var err error 62 | a.PathsCompiled, err = matchers.Glob(a.Paths...) 63 | if err != nil { 64 | return fmt.Errorf("failed to compile archive paths glob %q: %v", a.Paths, err) 65 | } 66 | 67 | if err := a.ReleaseSettings.Init(); err != nil { 68 | return fmt.Errorf("%s: %v", what, err) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | type ReleaseSettings struct { 75 | Type string `json:"type"` 76 | 77 | Name string `json:"name"` 78 | Repository string `json:"repository"` 79 | RepositoryOwner string `json:"repository_owner"` 80 | Draft bool `json:"draft"` 81 | Prerelease bool `json:"prerelease"` 82 | 83 | ReleaseNotesSettings ReleaseNotesSettings `json:"release_notes_settings"` 84 | 85 | TypeParsed releasetypes.Type `json:"-"` 86 | } 87 | 88 | type ReleaseNotesSettings struct { 89 | Generate bool `json:"generate"` 90 | GenerateOnHost bool `json:"generate_on_host"` 91 | Filename string `json:"filename"` 92 | TemplateFilename string `json:"template_filename"` 93 | Groups []ReleaseNotesGroup `json:"groups"` 94 | 95 | // Can be used to collapse releases with a few number (less than threshold) of changes into one title. 96 | ShortThreshold int `json:"short_threshold"` 97 | ShortTitle string `json:"short_title"` 98 | } 99 | 100 | func (g *ReleaseNotesSettings) Init() error { 101 | for i := range g.Groups { 102 | if err := g.Groups[i].Init(); err != nil { 103 | return fmt.Errorf("[%d]: %v", i, err) 104 | } 105 | } 106 | return nil 107 | } 108 | 109 | type ReleaseNotesGroup struct { 110 | Title string `json:"title"` 111 | Regexp string `json:"regexp"` 112 | Ignore bool `json:"ignore"` 113 | Ordinal int `json:"ordinal"` 114 | 115 | RegexpCompiled matchers.Matcher `json:"-"` 116 | } 117 | 118 | func (g *ReleaseNotesGroup) Init() error { 119 | what := "release.release_settings.group" 120 | if g.Regexp == "" { 121 | return fmt.Errorf("%s: regexp is not set", what) 122 | } 123 | 124 | if !strings.HasPrefix(g.Regexp, "(?") { 125 | g.Regexp = "(?i)" + g.Regexp 126 | } 127 | 128 | var err error 129 | re, err := regexp.Compile(g.Regexp) 130 | if err != nil { 131 | return fmt.Errorf("%s: %v", what, err) 132 | } 133 | 134 | g.RegexpCompiled = matchers.MatcherFunc(func(s string) bool { 135 | return re.MatchString(s) 136 | }) 137 | 138 | return nil 139 | } 140 | 141 | func (r *ReleaseSettings) Init() error { 142 | what := "release.release_settings" 143 | if r.Type == "" { 144 | return fmt.Errorf("%s: release type is not set", what) 145 | } 146 | 147 | var err error 148 | if r.TypeParsed, err = releasetypes.Parse(r.Type); err != nil { 149 | return fmt.Errorf("%s: %v", what, err) 150 | } 151 | 152 | if len(r.ReleaseNotesSettings.Groups) == 0 { 153 | // Add a default group matching all. 154 | r.ReleaseNotesSettings.Groups = []ReleaseNotesGroup{ 155 | { 156 | Title: "What's Changed", 157 | Regexp: ".*", 158 | }, 159 | } 160 | } 161 | 162 | if err := r.ReleaseNotesSettings.Init(); err != nil { 163 | return fmt.Errorf("%s: %v", what, err) 164 | } 165 | 166 | return nil 167 | } 168 | 169 | type Releases []Release 170 | -------------------------------------------------------------------------------- /internal/plugins/plugins.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugins 16 | 17 | import ( 18 | "strings" 19 | "time" 20 | 21 | "github.com/bep/execrpc" 22 | "github.com/bep/execrpc/codecs" 23 | "github.com/bep/logg" 24 | "github.com/gohugoio/hugoreleaser-plugins-api/archiveplugin" 25 | "github.com/gohugoio/hugoreleaser-plugins-api/model" 26 | "github.com/gohugoio/hugoreleaser/internal/config" 27 | ) 28 | 29 | // This represents a major version. 30 | // Increment this only when there are breaking changes to the plugin protocol. 31 | const pluginProtocolVersion = 2 32 | 33 | type ArchivePluginConfig struct { 34 | Infol logg.LevelLogger 35 | Try bool 36 | GoSettings config.GoSettings 37 | Options config.Plugin 38 | Project string 39 | Tag string 40 | } 41 | 42 | // StartArchivePlugin starts a archive plugin. 43 | func StartArchivePlugin(cfg ArchivePluginConfig) (*execrpc.Client[model.Config, archiveplugin.Request, any, model.Receipt], error) { 44 | env := cfg.Options.Env 45 | var hasGoProxy bool 46 | for _, e := range env { 47 | if strings.HasPrefix(e, "GOPROXY=") { 48 | hasGoProxy = true 49 | break 50 | } 51 | } 52 | if !hasGoProxy { 53 | env = append(env, "GOPROXY="+cfg.GoSettings.GoProxy) 54 | } 55 | 56 | serverCfg := model.Config{ 57 | Try: cfg.Try, 58 | ProjectInfo: model.ProjectInfo{ 59 | Project: cfg.Project, 60 | Tag: cfg.Tag, 61 | }, 62 | } 63 | 64 | client, err := execrpc.StartClient( 65 | execrpc.ClientOptions[model.Config, archiveplugin.Request, any, model.Receipt]{ 66 | Config: serverCfg, 67 | ClientRawOptions: execrpc.ClientRawOptions{ 68 | Version: pluginProtocolVersion, 69 | Cmd: cfg.GoSettings.GoExe, 70 | Args: []string{"run", cfg.Options.Command}, 71 | Dir: cfg.Options.Dir, 72 | Env: env, 73 | Timeout: 20 * time.Minute, 74 | }, 75 | Codec: codecs.TOMLCodec{}, 76 | }, 77 | ) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | go func() { 83 | for msg := range client.MessagesRaw() { 84 | statusCode := msg.Header.Status 85 | switch statusCode { 86 | case model.StatusInfoLog: 87 | cfg.Infol.Log(logg.String(string(msg.Body))) 88 | } 89 | } 90 | }() 91 | 92 | return client, nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/plugins/plugintypes/plugintypes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugintypes 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/gohugoio/hugoreleaser/internal/common/mapsh" 22 | ) 23 | 24 | // Type is the type of external tool. 25 | type Type int 26 | 27 | func (t Type) String() string { 28 | return typeString[t] 29 | } 30 | 31 | const ( 32 | // InvalidType is an invalid type. 33 | Invalid Type = iota 34 | 35 | // A external tool run via "go run ..." 36 | GoRun 37 | ) 38 | 39 | // Parse parses a string into a Type. 40 | func Parse(s string) (Type, error) { 41 | f := stringType[strings.ToLower(s)] 42 | if f == Invalid { 43 | return f, fmt.Errorf("invalid tool type %q, must be one of %s", s, mapsh.KeysSorted(typeString)) 44 | } 45 | return f, nil 46 | } 47 | 48 | // MustParse is like Parse but panics if the string is not a valid type. 49 | func MustParse(s string) Type { 50 | f, err := Parse(s) 51 | if err != nil { 52 | panic(err) 53 | } 54 | return f 55 | } 56 | 57 | var typeString = map[Type]string{ 58 | // The string values is what users can specify in the config. 59 | GoRun: "gorun", 60 | } 61 | 62 | var stringType = map[string]Type{} 63 | 64 | func init() { 65 | for k, v := range typeString { 66 | stringType[v] = k 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/plugins/plugintypes/plugintypes_test.go: -------------------------------------------------------------------------------- 1 | package plugintypes 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | ) 8 | 9 | func TestType(t *testing.T) { 10 | c := qt.New(t) 11 | 12 | c.Assert(MustParse("gorun"), qt.Equals, GoRun) 13 | c.Assert(MustParse("GoRun").String(), qt.Equals, "gorun") 14 | 15 | _, err := Parse("invalid") 16 | c.Assert(err, qt.ErrorMatches, "invalid tool type \"invalid\", must be one of .*") 17 | c.Assert(func() { MustParse("invalid") }, qt.PanicMatches, `invalid.*`) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /internal/releases/changelog/changelog.go: -------------------------------------------------------------------------------- 1 | package changelog 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "regexp" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // CollectChanges collects changes according to the given options. 13 | // If opts.ResolveUserName is set, it will be used to resolve Change.Username (e.g. GitHub login). 14 | func CollectChanges(opts Options) (Changes, error) { 15 | c := &collector{opts: opts} 16 | return c.collect() 17 | } 18 | 19 | // GroupByTitleFunc groups g by title according to the grouping function f. 20 | // If f returns false, that change item is not included in the result. 21 | func GroupByTitleFunc(g Changes, f func(Change) (string, int, bool)) ([]TitleChanges, error) { 22 | var ngi []TitleChanges 23 | for _, gi := range g { 24 | title, i, ok := f(gi) 25 | if !ok { 26 | continue 27 | } 28 | idx := -1 29 | for j, ngi := range ngi { 30 | if ngi.Title == title { 31 | idx = j 32 | break 33 | } 34 | } 35 | if idx == -1 { 36 | ngi = append(ngi, TitleChanges{Title: title, ordinal: i + 1}) 37 | idx = len(ngi) - 1 38 | } 39 | ngi[idx].Changes = append(ngi[idx].Changes, gi) 40 | } 41 | 42 | sort.Slice(ngi, func(i, j int) bool { 43 | return ngi[i].ordinal < ngi[j].ordinal 44 | }) 45 | 46 | return ngi, nil 47 | 48 | } 49 | 50 | // Change represents a git commit. 51 | type Change struct { 52 | // Fetched from git log. 53 | Hash string 54 | Author string 55 | Subject string 56 | Body string 57 | 58 | Issues []int 59 | 60 | // Resolved from GitHub. 61 | Username string 62 | } 63 | 64 | // Changes represents a list of git commits. 65 | type Changes []Change 66 | 67 | // Options for collecting changes. 68 | type Options struct { 69 | // Can be nil. 70 | // ResolveUserName returns the username for the given author (email address) and commit sha. 71 | // This is the GitHub login (e.g. bep) in its first iteration. 72 | ResolveUserName func(commit, author string) (string, error) 73 | 74 | // All of these can be empty. 75 | PrevTag string 76 | Tag string 77 | Commitish string 78 | RepoPath string 79 | } 80 | 81 | // TitleChanges represents a list of changes grouped by title. 82 | type TitleChanges struct { 83 | Title string 84 | Changes Changes 85 | 86 | ordinal int 87 | } 88 | 89 | type collector struct { 90 | opts Options 91 | } 92 | 93 | func (c *collector) collect() (Changes, error) { 94 | log, err := gitLog(c.opts.RepoPath, c.opts.PrevTag, c.opts.Tag, c.opts.Commitish) 95 | if err != nil { 96 | return nil, err 97 | } 98 | g, err := gitLogToGitInfos(log) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | if c.opts.ResolveUserName != nil { 104 | for i, gi := range g { 105 | username, err := c.opts.ResolveUserName(gi.Hash, gi.Author) 106 | if err != nil { 107 | return nil, err 108 | } 109 | g[i].Username = username 110 | } 111 | } 112 | 113 | return g, nil 114 | } 115 | 116 | func git(repo string, args ...string) (string, error) { 117 | if repo != "" { 118 | args = append([]string{"-C", repo}, args...) 119 | } 120 | 121 | cmd := exec.Command("git", args...) 122 | out, err := cmd.CombinedOutput() 123 | if err != nil { 124 | return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args) 125 | } 126 | return string(out), nil 127 | } 128 | 129 | func gitLog(repo, prevTag, tag, commitish string) (string, error) { 130 | var err error 131 | if prevTag != "" { 132 | exists, err := gitTagExists(repo, prevTag) 133 | if err != nil { 134 | return "", err 135 | } 136 | if !exists { 137 | return "", fmt.Errorf("prevTag %q does not exist", prevTag) 138 | } 139 | } 140 | 141 | if tag != "" { 142 | exists, err := gitTagExists(repo, tag) 143 | if err != nil { 144 | return "", err 145 | } 146 | if !exists { 147 | // Assume it hasn't been created yet, 148 | tag = "" 149 | } 150 | } 151 | 152 | var from, to string 153 | if prevTag != "" { 154 | from = prevTag 155 | } 156 | if tag != "" { 157 | to = tag 158 | } 159 | 160 | if to == "" { 161 | to = commitish 162 | } 163 | 164 | if from == "" { 165 | from, err = gitVersionTagBefore(repo, to) 166 | if err != nil { 167 | return "", err 168 | } 169 | } 170 | 171 | args := []string{"log", "--pretty=format:%x1e%h%x1f%aE%x1f%s%x1f%b", "--abbrev-commit", from + ".." + to} 172 | 173 | log, err := git(repo, args...) 174 | if err != nil { 175 | return ",", err 176 | } 177 | 178 | return log, err 179 | } 180 | 181 | func gitLogToGitInfos(log string) (Changes, error) { 182 | var g Changes 183 | log = strings.Trim(log, "\n\x1e'") 184 | entries := strings.Split(log, "\x1e") 185 | 186 | for _, entry := range entries { 187 | items := strings.Split(entry, "\x1f") 188 | var gi Change 189 | 190 | if len(items) > 0 { 191 | gi.Hash = items[0] 192 | } 193 | if len(items) > 1 { 194 | gi.Author = items[1] 195 | } 196 | if len(items) > 2 { 197 | gi.Subject = items[2] 198 | } 199 | if len(items) > 3 { 200 | gi.Body = items[3] 201 | 202 | // Parse issues. 203 | gi.Issues = parseIssues(gi.Body) 204 | } 205 | 206 | g = append(g, gi) 207 | } 208 | 209 | return g, nil 210 | } 211 | 212 | func gitShort(repo string, args ...string) (output string, err error) { 213 | output, err = git(repo, args...) 214 | return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err 215 | } 216 | 217 | func gitTagExists(repo, tag string) (bool, error) { 218 | out, err := git(repo, "tag", "-l", tag) 219 | if err != nil { 220 | return false, err 221 | } 222 | 223 | if strings.Contains(out, tag) { 224 | return true, nil 225 | } 226 | 227 | return false, nil 228 | } 229 | 230 | func gitVersionTagBefore(repo, ref string) (string, error) { 231 | if strings.HasPrefix(ref, "v") { 232 | ref += "^" 233 | } 234 | return gitShort(repo, "describe", "--tags", "--abbrev=0", "--always", "--match", "v[0-9]*", ref) 235 | } 236 | 237 | var issueRe = regexp.MustCompile(`(?i)(?:Updates?|Closes?|Fix.*|See) #(\d+)`) 238 | 239 | func parseIssues(body string) []int { 240 | var i []int 241 | m := issueRe.FindAllStringSubmatch(body, -1) 242 | for _, mm := range m { 243 | issueID, err := strconv.Atoi(mm[1]) 244 | if err != nil { 245 | continue 246 | } 247 | i = append(i, issueID) 248 | } 249 | return i 250 | } 251 | -------------------------------------------------------------------------------- /internal/releases/changelog/changelog_test.go: -------------------------------------------------------------------------------- 1 | package changelog 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | qt "github.com/frankban/quicktest" 8 | ) 9 | 10 | var isCI = os.Getenv("CI") != "" 11 | 12 | func TestGetOps(t *testing.T) { 13 | if isCI { 14 | // GitHub Actions clones shallowly, so we can't test this there. 15 | t.Skip("skip on CI") 16 | } 17 | c := qt.New(t) 18 | 19 | tag, err := gitVersionTagBefore("", "v0.51.0") 20 | c.Assert(err, qt.IsNil) 21 | c.Assert(tag, qt.Equals, "v0.50.0") 22 | 23 | exists, err := gitTagExists("", "v0.50.0") 24 | c.Assert(err, qt.IsNil) 25 | c.Assert(exists, qt.Equals, true) 26 | 27 | exists, err = gitTagExists("", "v3.9.0") 28 | c.Assert(err, qt.IsNil) 29 | c.Assert(exists, qt.Equals, false) 30 | 31 | log, err := gitLog("", "v0.50.0", "v0.51.0", "main") 32 | c.Assert(err, qt.IsNil) 33 | c.Assert(log, qt.Contains, "Shuffle chunked builds") 34 | 35 | infos, err := gitLogToGitInfos(log) 36 | c.Assert(err, qt.IsNil) 37 | c.Assert(len(infos), qt.Equals, 3) 38 | 39 | } 40 | 41 | // Issue 30. 42 | func TestGetVersionTagBefore(t *testing.T) { 43 | if isCI { 44 | // GitHub Actions clones shallowly, so we can't test this there. 45 | t.Skip("skip on CI") 46 | } 47 | c := qt.New(t) 48 | 49 | for _, test := range []struct { 50 | about string 51 | ref string 52 | expect string 53 | }{ 54 | { 55 | about: "patch tag", 56 | ref: "v0.53.2", 57 | expect: "v0.53.1", 58 | }, 59 | { 60 | about: "patch commit", 61 | ref: "8509f4591d37435df1bfb2bcb4dfb5fe474b0252", 62 | expect: "v0.53.2", 63 | }, 64 | { 65 | about: "minor", 66 | ref: "v0.52.0", 67 | expect: "v0.51.0", 68 | }, 69 | } { 70 | c.Run(test.about, func(c *qt.C) { 71 | tag, err := gitVersionTagBefore("", test.ref) 72 | c.Assert(err, qt.IsNil) 73 | c.Assert(tag, qt.Equals, test.expect) 74 | }) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /internal/releases/checksums.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package releases 16 | 17 | import ( 18 | "context" 19 | "crypto/sha256" 20 | "encoding/hex" 21 | "io" 22 | "os" 23 | "path/filepath" 24 | "sort" 25 | "sync" 26 | 27 | "github.com/bep/workers" 28 | ) 29 | 30 | // CreateChecksumLines writes the SHA256 checksums as lowercase hex digits followed by 31 | // two spaces and then the base of filename and returns a sorted slice. 32 | func CreateChecksumLines(w *workers.Workforce, filenames ...string) ([]string, error) { 33 | var mu sync.Mutex 34 | var result []string 35 | 36 | r, _ := w.Start(context.Background()) 37 | 38 | createChecksum := func(filename string) (string, error) { 39 | f, err := os.Open(filename) 40 | if err != nil { 41 | return "", err 42 | } 43 | defer f.Close() 44 | h := sha256.New() 45 | if _, err := io.Copy(h, f); err != nil { 46 | return "", err 47 | } 48 | return hex.EncodeToString(h.Sum(nil)), nil 49 | } 50 | 51 | for _, filename := range filenames { 52 | filename := filename 53 | r.Run(func() error { 54 | checksum, err := createChecksum(filename) 55 | if err != nil { 56 | return err 57 | } 58 | mu.Lock() 59 | result = append(result, checksum+" "+filepath.Base(filename)) 60 | mu.Unlock() 61 | 62 | return nil 63 | }) 64 | } 65 | 66 | if err := r.Wait(); err != nil { 67 | return nil, err 68 | } 69 | 70 | sort.Strings(result) 71 | 72 | return result, nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/releases/checksums_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package releases 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "runtime" 22 | "testing" 23 | 24 | "github.com/bep/workers" 25 | qt "github.com/frankban/quicktest" 26 | ) 27 | 28 | func TestCreateChecksumLines(t *testing.T) { 29 | c := qt.New(t) 30 | 31 | w := workers.New(runtime.NumCPU()) 32 | 33 | tempDir := t.TempDir() 34 | 35 | subDir := filepath.Join(tempDir, "sub") 36 | err := os.Mkdir(subDir, 0o755) 37 | c.Assert(err, qt.IsNil) 38 | 39 | var filenames []string 40 | 41 | for i := 0; i < 10; i++ { 42 | filename := filepath.Join(subDir, fmt.Sprintf("file%d.txt", i)) 43 | err := os.WriteFile(filename, []byte(fmt.Sprintf("hello%d", i)), 0o644) 44 | c.Assert(err, qt.IsNil) 45 | filenames = append(filenames, filename) 46 | } 47 | 48 | checksums, err := CreateChecksumLines(w, filenames...) 49 | c.Assert(err, qt.IsNil) 50 | c.Assert(checksums, qt.DeepEquals, []string{ 51 | "196373310827669cb58f4c688eb27aabc40e600dc98615bd329f410ab7430cff file6.txt", 52 | "47ea70cf08872bdb4afad3432b01d963ac7d165f6b575cd72ef47498f4459a90 file3.txt", 53 | "4e74512f1d8e5016f7a9d9eaebbeedb1549fed5b63428b736eecfea98292d75f file9.txt", 54 | "5a936ee19a0cf3c70d8cb0006111b7a52f45ec01703e0af8cdc8c6d81ac5850c file0.txt", 55 | "5d9dad16709372200908eecb6a67541ba4013bf7490ccb40d8b75832a1b4aca0 file7.txt", 56 | "87298cc2f31fba73181ea2a9e6ef10dce21ed95e98bdac9c4e1504ea16f486e4 file2.txt", 57 | "8dfe82d9a72ad831e48e524a38ad111f206ef08c39aa5847db26df034ee3b57d file5.txt", 58 | "91e9240f415223982edc345532630710e94a7f52cd5f48f5ee1afc555078f0ab file1.txt", 59 | "bd4c6c665a1b8b4745bcfd3d744ea37488237108681a8ba4486a76126327d3f2 file8.txt", 60 | "e361a57a7406adee653f1dcff660d84f0ca302907747af2a387f67821acfce33 file4.txt", 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /internal/releases/client.go: -------------------------------------------------------------------------------- 1 | package releases 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/gohugoio/hugoreleaser/internal/config" 8 | ) 9 | 10 | type ReleaseInfo struct { 11 | Project string 12 | Tag string 13 | Commitish string 14 | Settings config.ReleaseSettings 15 | } 16 | 17 | type Client interface { 18 | Release(ctx context.Context, info ReleaseInfo) (int64, error) 19 | UploadAssetsFile(ctx context.Context, info ReleaseInfo, f *os.File, releaseID int64) error 20 | } 21 | -------------------------------------------------------------------------------- /internal/releases/fakeclient.go: -------------------------------------------------------------------------------- 1 | package releases 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | ) 9 | 10 | // Fake client is only used in tests. 11 | type FakeClient struct { 12 | releaseID int64 13 | } 14 | 15 | func (c *FakeClient) Release(ctx context.Context, info ReleaseInfo) (int64, error) { 16 | // Tests depend on this string. 17 | fmt.Printf("fake: release: %#v\n", info) 18 | if info.Settings.ReleaseNotesSettings.Filename != "" { 19 | _, err := os.Stat(info.Settings.ReleaseNotesSettings.Filename) 20 | if err != nil { 21 | return 0, err 22 | } 23 | } 24 | c.releaseID = rand.Int63() 25 | return c.releaseID, nil 26 | } 27 | 28 | func (c *FakeClient) UploadAssetsFile(ctx context.Context, info ReleaseInfo, f *os.File, releaseID int64) error { 29 | if c.releaseID != releaseID { 30 | return fmt.Errorf("fake: releaseID mismatch: %d != %d", c.releaseID, releaseID) 31 | } 32 | if f == nil { 33 | return fmt.Errorf("fake: nil file") 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/releases/github.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package releases 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "net/http" 22 | "os" 23 | "path/filepath" 24 | "sync" 25 | 26 | "github.com/gohugoio/hugoreleaser/internal/releases/releasetypes" 27 | "github.com/google/go-github/v45/github" 28 | "golang.org/x/oauth2" 29 | ) 30 | 31 | const tokenEnvVar = "GITHUB_TOKEN" 32 | 33 | // Validate validates the release type. 34 | func Validate(typ releasetypes.Type) error { 35 | if typ != releasetypes.GitHub { 36 | return fmt.Errorf("release: only github is supported for now") 37 | } 38 | token := os.Getenv(tokenEnvVar) 39 | if token == "" { 40 | return fmt.Errorf("release: missing %q env var", tokenEnvVar) 41 | } 42 | return nil 43 | } 44 | 45 | func NewClient(ctx context.Context, typ releasetypes.Type) (Client, error) { 46 | if err := Validate(typ); err != nil { 47 | return nil, err 48 | } 49 | 50 | token := os.Getenv(tokenEnvVar) 51 | 52 | // Set in tests to test the all command. 53 | // and when running with the -try flag. 54 | if token == "faketoken" { 55 | return &FakeClient{}, nil 56 | } 57 | 58 | tokenSource := oauth2.StaticTokenSource( 59 | &oauth2.Token{AccessToken: token}, 60 | ) 61 | 62 | httpClient := oauth2.NewClient(ctx, tokenSource) 63 | 64 | return &GitHubClient{ 65 | client: github.NewClient(httpClient), 66 | usernameCache: make(map[string]string), 67 | }, nil 68 | } 69 | 70 | // UploadAssetsFileWithRetries is a wrapper around UploadAssetsFile that retries on temporary errors. 71 | func UploadAssetsFileWithRetries(ctx context.Context, client Client, info ReleaseInfo, releaseID int64, openFile func() (*os.File, error)) error { 72 | return withRetries(func() (error, bool) { 73 | f, err := openFile() 74 | if err != nil { 75 | return err, false 76 | } 77 | defer f.Close() 78 | err = client.UploadAssetsFile(ctx, info, f, releaseID) 79 | if err != nil && errors.Is(err, TemporaryError{}) { 80 | return err, true 81 | } 82 | return err, false 83 | }) 84 | } 85 | 86 | // UsernameResolver is an interface that allows to resolve the username of a commit. 87 | type UsernameResolver interface { 88 | ResolveUsername(ctx context.Context, sha, author string, info ReleaseInfo) (string, error) 89 | } 90 | 91 | var _ UsernameResolver = &GitHubClient{} 92 | 93 | type GitHubClient struct { 94 | client *github.Client 95 | 96 | usernameCacheMu sync.Mutex 97 | usernameCache map[string]string 98 | } 99 | 100 | func (c *GitHubClient) ResolveUsername(ctx context.Context, sha, author string, info ReleaseInfo) (string, error) { 101 | c.usernameCacheMu.Lock() 102 | defer c.usernameCacheMu.Unlock() 103 | if username, ok := c.usernameCache[author]; ok { 104 | return username, nil 105 | } 106 | r, resp, err := c.client.Repositories.GetCommit(ctx, info.Settings.RepositoryOwner, info.Settings.Repository, sha, nil) 107 | if err != nil { 108 | if resp != nil && (resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusUnprocessableEntity) { 109 | return "", nil 110 | } 111 | return "", err 112 | } 113 | if resp != nil && resp.StatusCode != http.StatusOK { 114 | return "", nil 115 | } 116 | 117 | if r.Author == nil || r.Author.Login == nil { 118 | return "", nil 119 | } 120 | 121 | c.usernameCache[author] = *r.Author.Login 122 | return c.usernameCache[author], nil 123 | } 124 | 125 | func (c *GitHubClient) Release(ctx context.Context, info ReleaseInfo) (int64, error) { 126 | s := func(s string) *string { 127 | if s == "" { 128 | return nil 129 | } 130 | return github.String(s) 131 | } 132 | 133 | settings := info.Settings 134 | 135 | var body string 136 | releaseNotesSettings := settings.ReleaseNotesSettings 137 | 138 | if releaseNotesSettings.Filename != "" { 139 | b, err := os.ReadFile(releaseNotesSettings.Filename) 140 | if err != nil { 141 | return 0, err 142 | } 143 | body = string(b) 144 | } 145 | 146 | // Truncate body. 147 | if len(body) > 100000 { 148 | body = body[:100000] 149 | } 150 | 151 | r := &github.RepositoryRelease{ 152 | TagName: s(info.Tag), 153 | TargetCommitish: s(info.Commitish), 154 | Name: s(settings.Name), 155 | Body: s(body), 156 | Draft: github.Bool(settings.Draft), 157 | Prerelease: github.Bool(settings.Prerelease), 158 | GenerateReleaseNotes: github.Bool(releaseNotesSettings.GenerateOnHost), 159 | } 160 | 161 | rel, resp, err := c.client.Repositories.CreateRelease(ctx, settings.RepositoryOwner, settings.Repository, r) 162 | if err != nil { 163 | return 0, err 164 | } 165 | 166 | if resp.StatusCode != http.StatusCreated { 167 | return 0, fmt.Errorf("github: unexpected status code: %d", resp.StatusCode) 168 | } 169 | 170 | return *rel.ID, nil 171 | } 172 | 173 | func (c *GitHubClient) UploadAssetsFile(ctx context.Context, info ReleaseInfo, f *os.File, releaseID int64) error { 174 | settings := info.Settings 175 | 176 | _, resp, err := c.client.Repositories.UploadReleaseAsset( 177 | ctx, 178 | settings.RepositoryOwner, 179 | settings.Repository, 180 | releaseID, 181 | &github.UploadOptions{ 182 | Name: filepath.Base(f.Name()), 183 | }, 184 | f, 185 | ) 186 | if err == nil { 187 | return nil 188 | } 189 | 190 | if resp != nil && !isTemporaryHttpStatus(resp.StatusCode) { 191 | return err 192 | } 193 | 194 | return TemporaryError{err} 195 | } 196 | 197 | type TemporaryError struct { 198 | error 199 | } 200 | 201 | // isTemporaryHttpStatus returns true if the status code is considered temporary, returning 202 | // true if not sure. 203 | func isTemporaryHttpStatus(status int) bool { 204 | switch status { 205 | case http.StatusUnprocessableEntity, http.StatusBadRequest: 206 | return false 207 | default: 208 | return true 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /internal/releases/releasetypes/releasetypes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package releasetypes 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/gohugoio/hugoreleaser/internal/common/mapsh" 22 | ) 23 | 24 | type Type int 25 | 26 | const ( 27 | InvalidType Type = iota 28 | GitHub 29 | ) 30 | 31 | var releaseTypeString = map[Type]string{ 32 | GitHub: "github", 33 | } 34 | 35 | var stringReleaseType = map[string]Type{} 36 | 37 | func init() { 38 | for k, v := range releaseTypeString { 39 | stringReleaseType[v] = k 40 | } 41 | } 42 | 43 | func (t Type) String() string { 44 | return releaseTypeString[t] 45 | } 46 | 47 | // Parse parses a string into a Type. 48 | func Parse(s string) (Type, error) { 49 | t := stringReleaseType[strings.ToLower(s)] 50 | if t == InvalidType { 51 | return t, fmt.Errorf("invalid release type %q, must be one of %s", s, mapsh.KeysSorted(releaseTypeString)) 52 | } 53 | return t, nil 54 | } 55 | 56 | // MustParse is like Parse but panics if the string is not a valid release type. 57 | func MustParse(s string) Type { 58 | t, err := Parse(s) 59 | if err != nil { 60 | panic(err) 61 | } 62 | return t 63 | } 64 | -------------------------------------------------------------------------------- /internal/releases/releasetypes/releasetypes_test.go: -------------------------------------------------------------------------------- 1 | package releasetypes 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | ) 8 | 9 | func TestType(t *testing.T) { 10 | c := qt.New(t) 11 | 12 | c.Assert(MustParse("Github"), qt.Equals, GitHub) 13 | 14 | _, err := Parse("invalid") 15 | c.Assert(err, qt.ErrorMatches, "invalid release type \"invalid\", must be one of .*") 16 | c.Assert(func() { MustParse("invalid") }, qt.PanicMatches, `invalid.*`) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /internal/releases/retry.go: -------------------------------------------------------------------------------- 1 | package releases 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | const numRetries = 10 9 | 10 | func withRetries(f func() (err error, shouldTryAgain bool)) error { 11 | var ( 12 | lastErr error 13 | nextInterval time.Duration = 77 * time.Millisecond 14 | ) 15 | 16 | for i := 0; i < numRetries; i++ { 17 | err, shouldTryAgain := f() 18 | if err == nil || !shouldTryAgain { 19 | return err 20 | } 21 | 22 | lastErr = err 23 | 24 | time.Sleep(nextInterval) 25 | nextInterval += time.Duration(rand.Int63n(int64(nextInterval))) 26 | } 27 | 28 | return lastErr 29 | } 30 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "flag" 21 | "fmt" 22 | "log" 23 | "os" 24 | "os/signal" 25 | "runtime/debug" 26 | "strings" 27 | "syscall" 28 | "time" 29 | 30 | "github.com/bep/logg" 31 | "github.com/gohugoio/hugoreleaser/cmd/allcmd" 32 | "github.com/gohugoio/hugoreleaser/cmd/archivecmd" 33 | "github.com/gohugoio/hugoreleaser/cmd/buildcmd" 34 | "github.com/gohugoio/hugoreleaser/cmd/corecmd" 35 | "github.com/gohugoio/hugoreleaser/cmd/releasecmd" 36 | "github.com/gohugoio/hugoreleaser/internal/common/logging" 37 | "github.com/peterbourgon/ff/v3" 38 | "github.com/peterbourgon/ff/v3/ffcli" 39 | "golang.org/x/sync/errgroup" 40 | ) 41 | 42 | var ( 43 | commit = "none" 44 | tag = "(devel)" 45 | date = "unknown" 46 | ) 47 | 48 | func main() { 49 | log.SetFlags(0) 50 | 51 | if err := parseAndRun(os.Args[1:]); err != nil { 52 | log.Fatal(err) 53 | } 54 | } 55 | 56 | func parseAndRun(args []string) (err error) { 57 | defer func() { 58 | if r := recover(); r != nil { 59 | fmt.Println("stacktrace from panic: \n" + string(debug.Stack())) 60 | err = fmt.Errorf("%v", r) 61 | } 62 | }() 63 | 64 | start := time.Now() 65 | 66 | var ( 67 | coreCommand, core = corecmd.New() 68 | buildCommand = buildcmd.New(core) 69 | archiveCommand = archivecmd.New(core) 70 | releaseCommand = releasecmd.New(core) 71 | allCommand = allcmd.New(core) 72 | ) 73 | 74 | coreCommand.Subcommands = []*ffcli.Command{ 75 | buildCommand, 76 | archiveCommand, 77 | releaseCommand, 78 | allCommand, 79 | newVersionCommand(), 80 | } 81 | 82 | opts := []ff.Option{ 83 | ff.WithEnvVarPrefix(corecmd.EnvPrefix), 84 | } 85 | 86 | coreCommand.Options = opts 87 | for _, subCommand := range coreCommand.Subcommands { 88 | subCommand.Options = opts 89 | } 90 | 91 | releaseCommand.Options = []ff.Option{ 92 | ff.WithEnvVarPrefix(corecmd.EnvPrefix), 93 | } 94 | 95 | defer func() { 96 | if closeErr := core.Close(); closeErr != nil && err == nil { 97 | err = fmt.Errorf("error closing app: %w", err) 98 | } 99 | elapsed := time.Since(start) 100 | s := logg.String(fmt.Sprintf("Total in %s …", logging.FormatBuildDuration(elapsed))) 101 | if core.InfoLog != nil { 102 | core.InfoLog.Log(s) 103 | } else { 104 | log.Print(s) 105 | } 106 | }() 107 | 108 | if err := core.PreInit(); err != nil { 109 | return fmt.Errorf("error in foo: %w", err) 110 | } 111 | 112 | if err := coreCommand.Parse(args); err != nil { 113 | return fmt.Errorf("error parsing command line: %w", err) 114 | } 115 | 116 | if core.Try { 117 | os.Setenv("GITHUB_TOKEN", "faketoken") 118 | } 119 | 120 | // Pass any non-empty flag value into the HUGORELEASER_ prefix in OS environment if not already set. 121 | coreCommand.FlagSet.VisitAll(func(f *flag.Flag) { 122 | envName := fmt.Sprintf("%s_%s", corecmd.EnvPrefix, strings.ToUpper(f.Name)) 123 | if os.Getenv(envName) == "" { 124 | if s := f.Value.String(); s != "" { 125 | os.Setenv(envName, f.Value.String()) 126 | } 127 | } 128 | }) 129 | 130 | ctx, cancel := context.WithTimeout(context.Background(), core.Timeout) 131 | ctx, _ = signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) 132 | defer cancel() 133 | done := make(chan struct{}) 134 | 135 | g, ctx := errgroup.WithContext(ctx) 136 | 137 | g.Go(func() error { 138 | if err := core.Init(); err != nil { 139 | return fmt.Errorf("error initializing config: %w", err) 140 | } 141 | if err := coreCommand.Run(ctx); err != nil { 142 | return fmt.Errorf("error running command: %w", err) 143 | } 144 | done <- struct{}{} 145 | return nil 146 | }) 147 | 148 | g.Go(func() error { 149 | for { 150 | select { 151 | case <-ctx.Done(): 152 | err := ctx.Err() 153 | if errors.Is(err, context.DeadlineExceeded) { 154 | log.Fatalf("command timed out after %s; increase -timeout if needed", core.Timeout) 155 | } 156 | return err 157 | case <-done: 158 | return nil 159 | } 160 | } 161 | }) 162 | 163 | err = g.Wait() 164 | 165 | return err 166 | } 167 | 168 | func newVersionCommand() *ffcli.Command { 169 | return &ffcli.Command{ 170 | Name: "version", 171 | ShortUsage: "hugoreleaser version", 172 | ShortHelp: "Print the version", 173 | LongHelp: "Print the version", 174 | Exec: func(context.Context, []string) error { 175 | initVersionInfo() 176 | fmt.Printf("hugoreleaser %v, commit %v, built at %v\n", tag, commit, date) 177 | return nil 178 | }, 179 | } 180 | } 181 | 182 | func initVersionInfo() { 183 | bi, ok := debug.ReadBuildInfo() 184 | if !ok { 185 | return 186 | } 187 | 188 | for _, s := range bi.Settings { 189 | switch s.Key { 190 | case "vcs": 191 | case "vcs.revision": 192 | commit = s.Value 193 | case "vcs.time": 194 | date = s.Value 195 | case "vcs.modified": 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Hugoreleaser Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "archive/tar" 19 | "bytes" 20 | "compress/gzip" 21 | "fmt" 22 | "io" 23 | "io/fs" 24 | "log" 25 | "os" 26 | "os/exec" 27 | "path/filepath" 28 | "regexp" 29 | "runtime" 30 | "strconv" 31 | "strings" 32 | "testing" 33 | "time" 34 | 35 | "github.com/bep/helpers/envhelpers" 36 | "github.com/bep/helpers/filehelpers" 37 | "github.com/rogpeppe/go-internal/testscript" 38 | ) 39 | 40 | // Note: If tests are running slow for you, make sure you have GOMODCACHE set. 41 | func TestCommands(t *testing.T) { 42 | setup := testSetupFunc() 43 | testscript.Run(t, testscript.Params{ 44 | Dir: "testscripts/commands", 45 | Setup: func(env *testscript.Env) error { 46 | return setup(env) 47 | }, 48 | }) 49 | } 50 | 51 | func TestMisc(t *testing.T) { 52 | setup := testSetupFunc() 53 | testscript.Run(t, testscript.Params{ 54 | Dir: "testscripts/misc", 55 | // UpdateScripts: true, 56 | Setup: func(env *testscript.Env) error { 57 | return setup(env) 58 | }, 59 | }) 60 | } 61 | 62 | // Tests in development can be put in "testscripts/unfinished". 63 | func TestUnfinished(t *testing.T) { 64 | if os.Getenv("CI") != "" { 65 | t.Skip("skip unfinished tests on CI") 66 | } 67 | 68 | setup := testSetupFunc() 69 | 70 | testscript.Run(t, testscript.Params{ 71 | Dir: "testscripts/unfinished", 72 | // TestWork: true, 73 | // UpdateScripts: true, 74 | Setup: func(env *testscript.Env) error { 75 | return setup(env) 76 | }, 77 | }) 78 | } 79 | 80 | func testSetupFunc() func(env *testscript.Env) error { 81 | sourceDir, _ := os.Getwd() 82 | return func(env *testscript.Env) error { 83 | var keyVals []string 84 | 85 | // SOURCE is where the hugoreleaser source code lives. 86 | // We do this so we can 87 | // 1. Copy the example/test plugins into the WORK dir where the test script is running. 88 | // 2. Append a replace directive to the plugins' go.mod to get the up-to-date version of the plugin API. 89 | // 90 | // This is a hybrid setup neeed to get a quick development cycle going. 91 | // In production, the plugin Go modules would be addressed on their full form, e.g. "github.com/gohugoio/hugoreleaser/internal/plugins/archives/tar@v1.0.0". 92 | keyVals = append(keyVals, "SOURCE", sourceDir) 93 | keyVals = append(keyVals, "GOCACHE", filepath.Join(env.WorkDir, "gocache")) 94 | var gomodCache string 95 | if c := os.Getenv("GOMODCACHE"); c != "" { 96 | // Testscripts will set the GOMODCACHE to an empty dir, 97 | // and this slows down some tests considerably. 98 | // Use the OS env var if it is set. 99 | gomodCache = c 100 | } else { 101 | gomodCache = filepath.Join(env.WorkDir, "gomodcache") 102 | } 103 | keyVals = append(keyVals, "GOMODCACHE", gomodCache) 104 | 105 | envhelpers.SetEnvVars(&env.Vars, keyVals...) 106 | 107 | return nil 108 | } 109 | } 110 | 111 | func TestMain(m *testing.M) { 112 | os.Exit( 113 | testscript.RunMain(m, map[string]func() int{ 114 | // The main program. 115 | "hugoreleaser": func() int { 116 | if err := parseAndRun(os.Args[1:]); err != nil { 117 | fmt.Fprintln(os.Stderr, err) 118 | return 1 119 | } 120 | return 0 121 | }, 122 | 123 | // dostounix converts \r\n to \n. 124 | "dostounix": func() int { 125 | filename := os.Args[1] 126 | b, err := os.ReadFile(filename) 127 | if err != nil { 128 | fatalf("%v", err) 129 | } 130 | b = bytes.Replace(b, []byte("\r\n"), []byte{'\n'}, -1) 131 | if err := os.WriteFile(filename, b, 0o666); err != nil { 132 | fatalf("%v", err) 133 | } 134 | return 0 135 | }, 136 | 137 | // log prints to stderr. 138 | "log": func() int { 139 | log.Println(os.Args[1]) 140 | return 0 141 | }, 142 | "sleep": func() int { 143 | i, err := strconv.Atoi(os.Args[1]) 144 | if err != nil { 145 | i = 1 146 | } 147 | time.Sleep(time.Duration(i) * time.Second) 148 | return 0 149 | }, 150 | 151 | // ls lists a directory to stdout. 152 | "ls": func() int { 153 | dirname := os.Args[1] 154 | dir, err := os.Open(dirname) 155 | if err != nil { 156 | fatalf("%v", err) 157 | } 158 | fis, err := dir.Readdir(-1) 159 | if err != nil { 160 | fatalf("%v", err) 161 | } 162 | for _, fi := range fis { 163 | fmt.Printf("%s %04o %s\n", fi.Mode(), fi.Mode().Perm(), fi.Name()) 164 | } 165 | return 0 166 | }, 167 | 168 | // printarchive prints the contents of an archive to stdout. 169 | "printarchive": func() int { 170 | archiveFilename := os.Args[1] 171 | 172 | if !strings.HasSuffix(archiveFilename, ".tar.gz") { 173 | fatalf("only .tar.gz supported for now, got: %q", archiveFilename) 174 | } 175 | 176 | f, err := os.Open(archiveFilename) 177 | if err != nil { 178 | fatalf("%v", err) 179 | } 180 | defer f.Close() 181 | 182 | gr, err := gzip.NewReader(f) 183 | if err != nil { 184 | fatalf("%v", err) 185 | } 186 | defer gr.Close() 187 | tr := tar.NewReader(gr) 188 | 189 | for { 190 | hdr, err := tr.Next() 191 | if err == io.EOF { 192 | break 193 | } 194 | if err != nil { 195 | fatalf("%v", err) 196 | } 197 | mode := fs.FileMode(hdr.Mode) 198 | fmt.Printf("%s %04o %s\n", mode, mode.Perm(), hdr.Name) 199 | } 200 | 201 | return 0 202 | }, 203 | 204 | // cpdir copies a file. 205 | "cpfile": func() int { 206 | if len(os.Args) != 3 { 207 | fmt.Fprintln(os.Stderr, "usage: cpdir SRC DST") 208 | return 1 209 | } 210 | 211 | fromFile := os.Args[1] 212 | toFile := os.Args[2] 213 | 214 | if !filepath.IsAbs(fromFile) { 215 | fromFile = filepath.Join(os.Getenv("SOURCE"), fromFile) 216 | } 217 | 218 | if err := os.MkdirAll(filepath.Dir(toFile), 0o755); err != nil { 219 | fmt.Fprintln(os.Stderr, err) 220 | return 1 221 | } 222 | 223 | err := filehelpers.CopyFile(fromFile, toFile) 224 | if err != nil { 225 | fmt.Fprintln(os.Stderr, err) 226 | return 1 227 | } 228 | return 0 229 | }, 230 | 231 | // cpdir copies a directory recursively. 232 | "cpdir": func() int { 233 | if len(os.Args) != 3 { 234 | fmt.Fprintln(os.Stderr, "usage: cpdir SRC DST") 235 | return 1 236 | } 237 | 238 | fromDir := os.Args[1] 239 | toDir := os.Args[2] 240 | 241 | if !filepath.IsAbs(fromDir) { 242 | fromDir = filepath.Join(os.Getenv("SOURCE"), fromDir) 243 | } 244 | 245 | err := filehelpers.CopyDir(fromDir, toDir, nil) 246 | if err != nil { 247 | fmt.Fprintln(os.Stderr, err) 248 | return 1 249 | } 250 | return 0 251 | }, 252 | 253 | // append appends to a file with a leaading newline. 254 | "append": func() int { 255 | if len(os.Args) < 3 { 256 | 257 | fmt.Fprintln(os.Stderr, "usage: append FILE TEXT") 258 | return 1 259 | } 260 | 261 | filename := os.Args[1] 262 | words := os.Args[2:] 263 | for i, word := range words { 264 | words[i] = strings.Trim(word, "\"") 265 | } 266 | text := strings.Join(words, " ") 267 | 268 | _, err := os.Stat(filename) 269 | if err != nil { 270 | if os.IsNotExist(err) { 271 | fmt.Fprintln(os.Stderr, "file does not exist:", filename) 272 | return 1 273 | } 274 | fmt.Fprintln(os.Stderr, err) 275 | return 1 276 | } 277 | 278 | f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0o644) 279 | if err != nil { 280 | fmt.Fprintln(os.Stderr, "failed to open file:", filename) 281 | return 1 282 | } 283 | defer f.Close() 284 | 285 | _, err = f.WriteString("\n" + text) 286 | if err != nil { 287 | fmt.Fprintln(os.Stderr, "failed to write to file:", filename) 288 | return 1 289 | } 290 | 291 | return 0 292 | }, 293 | 294 | // Helpers. 295 | "checkfile": func() int { 296 | // The built-in exists does not check for zero size files. 297 | args := os.Args[1:] 298 | var readonly, exec bool 299 | loop: 300 | for len(args) > 0 { 301 | switch args[0] { 302 | case "-readonly": 303 | readonly = true 304 | args = args[1:] 305 | case "-exec": 306 | exec = true 307 | args = args[1:] 308 | default: 309 | break loop 310 | } 311 | } 312 | if len(args) == 0 { 313 | fatalf("usage: checkfile [-readonly] [-exec] file...") 314 | } 315 | 316 | for _, filename := range args { 317 | 318 | fi, err := os.Stat(filename) 319 | if err != nil { 320 | fmt.Fprintf(os.Stderr, "stat %s: %v\n", filename, err) 321 | return -1 322 | } 323 | if fi.Size() == 0 { 324 | fmt.Fprintf(os.Stderr, "%s is empty\n", filename) 325 | return -1 326 | } 327 | if readonly && fi.Mode()&0o222 != 0 { 328 | fmt.Fprintf(os.Stderr, "%s is writable\n", filename) 329 | return -1 330 | } 331 | if exec && runtime.GOOS != "windows" && fi.Mode()&0o111 == 0 { 332 | fmt.Fprintf(os.Stderr, "%s is not executable\n", filename) 333 | return -1 334 | } 335 | } 336 | 337 | return 0 338 | }, 339 | "checkfilecount": func() int { 340 | if len(os.Args) != 3 { 341 | fatalf("usage: checkfilecount count dir") 342 | } 343 | 344 | count, err := strconv.Atoi(os.Args[1]) 345 | if err != nil { 346 | fatalf("invalid count: %v", err) 347 | } 348 | if count < 0 { 349 | fatalf("count must be non-negative") 350 | } 351 | dir := os.Args[2] 352 | 353 | found := 0 354 | 355 | filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 356 | if err != nil { 357 | return err 358 | } 359 | if d.IsDir() { 360 | return nil 361 | } 362 | found++ 363 | return nil 364 | }) 365 | 366 | if found != count { 367 | fmt.Fprintf(os.Stderr, "found %d files, want %d\n", found, count) 368 | return -1 369 | } 370 | 371 | return 0 372 | }, 373 | "gobinary": func() int { 374 | if runtime.GOOS == "windows" { 375 | return 0 376 | } 377 | if len(os.Args) < 3 { 378 | fatalf("usage: gobinary binary args...") 379 | } 380 | 381 | filename := os.Args[1] 382 | pattern := os.Args[2] 383 | if !strings.HasPrefix(pattern, "(") { 384 | // Multiline matching. 385 | pattern = "(?s)" + pattern 386 | } 387 | re := regexp.MustCompile(pattern) 388 | 389 | cmd := exec.Command("go", "version", "-m", filename) 390 | cmd.Stderr = os.Stderr 391 | 392 | b, err := cmd.Output() 393 | if err != nil { 394 | fmt.Fprintln(os.Stderr, err) 395 | return -1 396 | } 397 | 398 | output := string(b) 399 | 400 | if !re.MatchString(output) { 401 | fmt.Fprintf(os.Stderr, "expected %q to match %q\n", output, re) 402 | return -1 403 | } 404 | 405 | return 0 406 | }, 407 | }), 408 | ) 409 | } 410 | 411 | func fatalf(format string, a ...any) { 412 | panic(fmt.Sprintf(format, a...)) 413 | } 414 | -------------------------------------------------------------------------------- /maintenance/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gohugoio/hugoreleaser/maintenance 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/abhinav/goldmark-toc v0.2.1 // indirect 7 | github.com/yuin/goldmark v1.4.13 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /maintenance/go.sum: -------------------------------------------------------------------------------- 1 | github.com/abhinav/goldmark-toc v0.2.1 h1:QJsKKGbdVeCWYMB11hSkNuZLuIzls7Y4KBZfwTkBB90= 2 | github.com/abhinav/goldmark-toc v0.2.1/go.mod h1:aq1IZ9qN85uFYpowec98iJrFkEHYT4oeFD1SC0qd8d0= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 7 | github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 8 | github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= 9 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /maintenance/readmetoc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/yuin/goldmark" 11 | "github.com/yuin/goldmark/ast" 12 | "github.com/yuin/goldmark/parser" 13 | "github.com/yuin/goldmark/renderer" 14 | "github.com/yuin/goldmark/util" 15 | ) 16 | 17 | func main() { 18 | readmeFilename := "../README.md" 19 | readmeContent, err := os.ReadFile(readmeFilename) 20 | must(err) 21 | 22 | toc, err := createToc(string(readmeContent)) 23 | must(err) 24 | 25 | fmt.Println(toc) 26 | } 27 | 28 | func createToc(s string) (string, error) { 29 | r := renderer.NewRenderer(renderer.WithNodeRenderers(util.Prioritized(&tocRenderer{}, 1000))) 30 | markdown := goldmark.New( 31 | goldmark.WithParserOptions(parser.WithAutoHeadingID()), 32 | goldmark.WithRenderer(r), 33 | ) 34 | 35 | var buff bytes.Buffer 36 | if err := markdown.Convert([]byte(s), &buff); err != nil { 37 | return "", err 38 | } 39 | 40 | return buff.String(), nil 41 | } 42 | 43 | type tocRenderer struct { 44 | } 45 | 46 | func (r *tocRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 47 | parent := node.Parent() 48 | if !entering || parent.Kind() != ast.KindHeading { 49 | return ast.WalkContinue, nil 50 | } 51 | 52 | hn := parent.(*ast.Heading) 53 | 54 | idattr, ok := hn.AttributeString("id") 55 | if !ok { 56 | return ast.WalkSkipChildren, nil 57 | } 58 | 59 | id := string(idattr.([]byte)) 60 | 61 | n := node.(*ast.Text) 62 | segment := n.Segment 63 | 64 | // Start at level 2. 65 | numIndentation := hn.Level - 2 66 | if numIndentation < 0 { 67 | return ast.WalkContinue, nil 68 | } 69 | 70 | fmt.Fprintf(w, "%s * [%s](#%s)\n", strings.Repeat(" ", numIndentation*4), string(segment.Value(source)), id) 71 | 72 | return ast.WalkContinue, nil 73 | } 74 | 75 | func (r *tocRenderer) renderNoop(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 76 | return ast.WalkContinue, nil 77 | } 78 | 79 | func (r *tocRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 80 | // I'm sure there must simpler way of doing this ... 81 | reg.Register(ast.KindText, r.renderText) 82 | 83 | // Ignore everything else. 84 | reg.Register(ast.KindHeading, r.renderNoop) 85 | reg.Register(ast.KindDocument, r.renderNoop) 86 | reg.Register(ast.KindBlockquote, r.renderNoop) 87 | reg.Register(ast.KindCodeBlock, r.renderNoop) 88 | reg.Register(ast.KindFencedCodeBlock, r.renderNoop) 89 | reg.Register(ast.KindHTMLBlock, r.renderNoop) 90 | reg.Register(ast.KindList, r.renderNoop) 91 | reg.Register(ast.KindListItem, r.renderNoop) 92 | reg.Register(ast.KindParagraph, r.renderNoop) 93 | reg.Register(ast.KindTextBlock, r.renderNoop) 94 | reg.Register(ast.KindThematicBreak, r.renderNoop) 95 | reg.Register(ast.KindAutoLink, r.renderNoop) 96 | reg.Register(ast.KindCodeSpan, r.renderNoop) 97 | reg.Register(ast.KindEmphasis, r.renderNoop) 98 | reg.Register(ast.KindImage, r.renderNoop) 99 | reg.Register(ast.KindLink, r.renderNoop) 100 | reg.Register(ast.KindRawHTML, r.renderNoop) 101 | 102 | reg.Register(ast.KindString, r.renderNoop) 103 | 104 | } 105 | 106 | func (r *tocRenderer) AddOptions(...renderer.Option) { 107 | 108 | } 109 | 110 | func must(err error) { 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /maintenance/readmetoc_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | qt "github.com/frankban/quicktest" 8 | ) 9 | 10 | func TestReadMeTOC(t *testing.T) { 11 | c := qt.New(t) 12 | 13 | doc := ` 14 | 15 | ## Heading 1 16 | 17 | Some text. 18 | 19 | ### Headings 2 20 | 21 | Some text. 22 | 23 | ## Headings 1-2 24 | 25 | 26 | ` 27 | 28 | toc, err := createToc(doc) 29 | c.Assert(err, qt.IsNil) 30 | 31 | c.Assert(strings.TrimSpace(toc), qt.Equals, "* [Heading 1](#heading-1)\n * [Headings 2](#headings-2)\n * [Headings 1-2](#headings-1-2)") 32 | 33 | } 34 | -------------------------------------------------------------------------------- /staticfiles/templates.go: -------------------------------------------------------------------------------- 1 | package staticfiles 2 | 3 | import ( 4 | _ "embed" 5 | "text/template" 6 | 7 | "github.com/gohugoio/hugoreleaser/internal/common/templ" 8 | ) 9 | 10 | var ( 11 | //go:embed templates/release-notes.gotmpl 12 | releaseNotesTemplContent []byte 13 | 14 | // ReleaseNotesTemplate is the template for the release notes. 15 | ReleaseNotesTemplate *template.Template 16 | ) 17 | 18 | func init() { 19 | ReleaseNotesTemplate = template.Must(template.New("release-notes").Parse(string(releaseNotesTemplContent))).Funcs(templ.BuiltInFuncs) 20 | } 21 | -------------------------------------------------------------------------------- /staticfiles/templates/release-notes.gotmpl: -------------------------------------------------------------------------------- 1 | {{ range .ChangeGroups -}} 2 | ## {{ .Title }} 3 | 4 | {{ range .Changes -}} 5 | * {{ .Subject }} {{ .Hash }}{{ with .Username }} @{{ . }}{{ end }} {{ range .Issues }}#{{ . }} {{ end }} 6 | {{ end }} 7 | {{ end }} 8 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | go install 2 | hugoreleaser archive -tag v0.58.0 -------------------------------------------------------------------------------- /testscripts/commands/all.txt: -------------------------------------------------------------------------------- 1 | 2 | env READMEFILE=README.md 3 | # faketoken is a magic string that will create a FakeClient. 4 | env GITHUB_TOKEN=faketoken 5 | 6 | hugoreleaser all -tag v1.2.0 -commitish main 7 | ! stderr . 8 | 9 | stdout 'Prepared 2 files' 10 | stdout 'Uploading' 11 | 12 | # Test files 13 | -- hugoreleaser.yaml -- 14 | project: hugo 15 | go_settings: 16 | go_proxy: https://proxy.golang.org 17 | go_exe: go 18 | build_settings: 19 | binary: hugo 20 | release_settings: 21 | type: github 22 | repository: hugoreleaser 23 | repository_owner: bep 24 | draft: true 25 | archive_settings: 26 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 27 | extra_files: 28 | - source_path: README.md 29 | target_path: README.md 30 | - source_path: license.txt 31 | target_path: license.txt 32 | type: 33 | format: tar.gz 34 | extension: .tar.gz 35 | builds: 36 | - path: mac 37 | os: 38 | - goos: darwin 39 | archs: 40 | - goarch: arm64 41 | archives: 42 | - paths: 43 | - builds/mac/** 44 | releases: 45 | - paths: 46 | - archives/** 47 | path: myrelease 48 | 49 | 50 | -- go.mod -- 51 | module foo 52 | -- main.go -- 53 | package main 54 | func main() { 55 | 56 | } 57 | -- README.md -- 58 | This is readme. 59 | -- license.txt -- 60 | This is license. -------------------------------------------------------------------------------- /testscripts/commands/build_and_archive.txt: -------------------------------------------------------------------------------- 1 | 2 | env READMEFILE=README.md 3 | 4 | # Change permissions of one of the files 5 | chmod 643 license.txt 6 | 7 | # Build binaries. 8 | hugoreleaser build -tag v1.2.0 9 | ! stderr . 10 | 11 | exists $WORK/dist/hugo/v1.2.0/builds/main/base/darwin/amd64/hugo 12 | exists $WORK/dist/hugo/v1.2.0/builds/main/base/darwin/arm64/hugo 13 | exists $WORK/dist/hugo/v1.2.0/builds/main/base/linux/amd64/hugo 14 | exists $WORK/dist/hugo/v1.2.0/builds/main/base/linux/arm/hugo 15 | exists $WORK/dist/hugo/v1.2.0/builds/main/base/windows/amd64/hugo.exe 16 | 17 | # Check Go binaries vs build settings. 18 | gobinary $WORK/dist/hugo/v1.2.0/builds/main/base/darwin/amd64/hugo CGO_ENABLED=0.*GOARCH=amd64\b.*GOOS=darwin 19 | gobinary $WORK/dist/hugo/v1.2.0/builds/main/base/darwin/arm64/hugo CGO_ENABLED=0.*GOARCH=arm64\b.*GOOS=darwin 20 | gobinary $WORK/dist/hugo/v1.2.0/builds/main/base/linux/arm/hugo CGO_ENABLED=0.*GOARCH=arm\b 21 | gobinary $WORK/dist/hugo/v1.2.0/builds/main/base/windows/amd64/hugo.exe CGO_ENABLED=0 22 | 23 | # Build archives 24 | hugoreleaser archive -tag v1.2.0 25 | ! stderr . 26 | stdout 'Archive file.*macOS-64bit' 27 | 28 | ls $WORK/dist/hugo/v1.2.0/builds/main/base/darwin/amd64 29 | # Re. Windows, see comment below. 30 | [!windows] stdout '-rwxr-xr-x 0755 hugo' 31 | 32 | # Check some samples. 33 | exists $WORK/dist/hugo/v1.2.0/archives/main/base/darwin/amd64/hugo_1.2.0_macOS-64bit.tar.gz 34 | exists $WORK/dist/hugo/v1.2.0/archives/main/base/linux/amd64/hugo_1.2.0_linux-64bit.tar.gz 35 | exists $WORK/dist/hugo/v1.2.0/archives/main/base/windows/amd64/hugo_1.2.0_Windows-64bit.zip 36 | ! exists $WORK/dist/hugo/v1.2.0/archives/main/base/linux/amd64/hugo_1.2.0_linux-64bit.zip 37 | ! exists $WORK/dist/hugo/v1.2.0/archives/main/base/darwin/arm64/hugo_1.2.0_macOS-ARM64.tar.gz 38 | 39 | printarchive $WORK/dist/hugo/v1.2.0/archives/main/base/linux/amd64/hugo_1.2.0_linux-64bit.tar.gz 40 | # This prints 0666 hugo on Windows 41 | # This is a big topic that I'm not prepared to take on now, see https://github.com/golang/go/issues/41809 42 | [!windows] stdout '-rwxr-xr-x 0755 hugo' 43 | 44 | # TODO(bep) check why these fail on Windows. 45 | # It looks like nothing gets printed, so I suspect it's printarchive that somehow fails. 46 | [!windows] stdout '-rw-r--r-- 0644 README.md' 47 | [!windows] stdout '-rw-r---wx 0643 license.txt' 48 | [!windows] stdout '-rw-r---w- 0642 subdir/myconfig.yaml' 49 | 50 | # Test files 51 | -- hugoreleaser.yaml -- 52 | project: hugo 53 | build_settings: 54 | binary: hugo 55 | archive_settings: 56 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 57 | extra_files: 58 | - source_path: ${READMEFILE} 59 | target_path: README.md 60 | - source_path: license.txt 61 | target_path: license.txt 62 | - source_path: hugoreleaser.yaml 63 | target_path: subdir/myconfig.yaml 64 | mode: 418 65 | type: 66 | format: tar.gz 67 | extension: .tar.gz 68 | replacements: 69 | "386": 32bit 70 | amd64: 64bit 71 | arm64: ARM64 72 | darwin: macOS 73 | windows: Windows 74 | builds: 75 | - path: main/base 76 | build_settings: 77 | env: 78 | - CGO_ENABLED=0 79 | ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio 80 | flags: 81 | - -buildmode 82 | - exe 83 | os: 84 | - goos: darwin 85 | archs: 86 | - goarch: amd64 87 | - goarch: arm64 88 | - goos: linux 89 | archs: 90 | - goarch: amd64 91 | - goarch: arm 92 | - goos: windows 93 | build_settings: 94 | binary: hugo.exe 95 | archs: 96 | - goarch: amd64 97 | archives: 98 | - paths: 99 | - builds/**/{darwin,linux}/amd64 100 | archive_settings: {} 101 | - paths: 102 | - builds/**/windows/* 103 | archive_settings: 104 | type: 105 | format: zip 106 | extension: .zip 107 | 108 | 109 | -- go.mod -- 110 | module foo 111 | -- main.go -- 112 | package main 113 | func main() { 114 | 115 | } 116 | -- README.md -- 117 | This is readme. 118 | -- license.txt -- 119 | This is license. -------------------------------------------------------------------------------- /testscripts/commands/release.txt: -------------------------------------------------------------------------------- 1 | 2 | env READMEFILE=README.md 3 | env GITHUB_TOKEN=faketoken 4 | 5 | # Skip build, use these fake binaries. 6 | # Txtar produces \r\n on Windows, which does not work well with checksums, so normalize the expected file set here. 7 | dostounix dist/hugo/v1.2.0/builds/main/base/windows/amd64/hugo.exe 8 | dostounix dist/hugo/v1.2.0/builds/main/base/darwin/arm64/hugo 9 | dostounix dist/hugo/v1.2.0/builds/main/base/darwin/amd64/hugo 10 | dostounix dist/hugo/v1.2.0/builds/main/base/linux/amd64/hugo 11 | dostounix dist/hugo/v1.2.0/builds/main/base/linux/arm/hugo 12 | dostounix expected/dist/myrelease/checksums.txt 13 | 14 | # Build archives 15 | hugoreleaser archive -tag v1.2.0 16 | ! stderr . 17 | exists $WORK/dist/hugo/v1.2.0/archives/main/base/darwin/amd64/hugo_1.2.0_macOS-64bit.tar.gz 18 | 19 | # Run with the a faketoken to avoid actually creating a remote release. 20 | hugoreleaser release -tag v1.2.0 -commitish main 21 | # 3 archives + checksums.txt 22 | stdout 'Prepared 4 files to archive' 23 | stdout 'hugo_1.2.0_checksums\.txt' 24 | cmp $WORK/dist/hugo/v1.2.0/releases/myrelease/hugo_1.2.0_checksums.txt $WORK/expected/dist/myrelease/checksums.txt 25 | 26 | # Test files 27 | # Release notes 28 | -- temp/my-release-notes.md -- 29 | ## Release notes 30 | * Change 1 31 | # Fake binaries to get stable archive checksums.txt. 32 | -- dist/hugo/v1.2.0/builds/main/base/windows/amd64/hugo.exe -- 33 | win-amd64 34 | -- dist/hugo/v1.2.0/builds/main/base/darwin/arm64/hugo -- 35 | darwin-armd64 36 | -- dist/hugo/v1.2.0/builds/main/base/darwin/amd64/hugo -- 37 | darwin-amd64 38 | -- dist/hugo/v1.2.0/builds/main/base/linux/amd64/hugo -- 39 | linux-amd64 40 | -- dist/hugo/v1.2.0/builds/main/base/linux/arm/hugo -- 41 | linux-amd 42 | 43 | # Expected output 44 | -- expected/dist/myrelease/checksums.txt -- 45 | 19c2308936cdc630dfb1c3620d54fc22dc072dd6d04f8aa0872963c3fb547572 hugo_1.2.0_Windows-64bit.zip 46 | 8a49e492c1b787821fe81695617dcaf211ca3c0428094f3a4a4c1401678993a0 hugo_1.2.0_macOS-64bit.tar.gz 47 | df51345af47d4122b133055aa8bb6109cc47504026c29634b0a6e77f6aa7ebcf hugo_1.2.0_linux-64bit.tar.gz 48 | -- hugoreleaser.yaml -- 49 | project: hugo 50 | release_settings: 51 | type: github 52 | repository: hugoreleaser 53 | repository_owner: bep 54 | draft: true 55 | release_notes_settings: 56 | filename: temp/my-release-notes.md 57 | build_settings: 58 | binary: hugo 59 | flags: 60 | - -trimpath 61 | archive_settings: 62 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 63 | extra_files: 64 | - source_path: ${READMEFILE} 65 | target_path: README.md 66 | - source_path: license.txt 67 | target_path: license.txt 68 | type: 69 | format: rename 70 | extension: .tar.gz 71 | replacements: 72 | "386": 32bit 73 | amd64: 64bit 74 | arm64: ARM64 75 | darwin: macOS 76 | windows: Windows 77 | builds: 78 | - path: main/base 79 | build_settings: 80 | env: 81 | - CGO_ENABLED=0 82 | ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio 83 | flags: 84 | - -buildmode 85 | - exe 86 | os: 87 | - goos: darwin 88 | archs: 89 | - goarch: amd64 90 | - goarch: arm64 91 | - goos: linux 92 | archs: 93 | - goarch: amd64 94 | - goarch: arm 95 | - goos: windows 96 | build_settings: 97 | binary: hugo.exe 98 | archs: 99 | - goarch: amd64 100 | archives: 101 | - paths: 102 | - builds/**/{darwin,linux}/amd64 103 | archive_settings: {} 104 | - paths: 105 | - builds/**/windows/* 106 | archive_settings: 107 | type: 108 | format: rename 109 | extension: .zip 110 | releases: 111 | - paths: 112 | - archives/** 113 | path: myrelease 114 | 115 | 116 | -- go.mod -- 117 | module foo 118 | -- main.go -- 119 | package main 120 | func main() { 121 | 122 | } 123 | -- README.md -- 124 | This is readme. 125 | -- license.txt -- 126 | This is license. 127 | -------------------------------------------------------------------------------- /testscripts/misc/archive_alias_replacements.txt: -------------------------------------------------------------------------------- 1 | env GOPATH=$WORK/gopath 2 | env GITHUB_TOKEN=faketoken 3 | 4 | # Skip build, use these fake binaries. 5 | dostounix dist/hugo/v1.2.0/builds/main/base/linux/amd64/hugo 6 | dostounix expected/dist/myrelease/checksums.txt 7 | 8 | # Build archives 9 | hugoreleaser archive -tag v1.2.0 10 | ! stderr . 11 | exists $WORK/dist/hugo/v1.2.0/archives/main/base/linux/amd64/hugo_1.2.0_linux-64bit.tar.gz 12 | exists $WORK/dist/hugo/v1.2.0/archives/main/base/linux/amd64/hugo_1.2.0_linux-amd64-alias.tar.gz 13 | 14 | # Run with the a faketoken to avoid actually creating a remote release. 15 | hugoreleaser release -tag v1.2.0 -commitish main 16 | # 2 archives + checksums.txt 17 | stdout 'Prepared 3 files to archive' 18 | stdout 'hugo_1.2.0_checksums\.txt' 19 | cmp $WORK/dist/hugo/v1.2.0/releases/myrelease/hugo_1.2.0_checksums.txt $WORK/expected/dist/myrelease/checksums.txt 20 | 21 | # Test files 22 | # Release notes 23 | -- temp/my-release-notes.md -- 24 | ## Release notes 25 | * Change 1 26 | # Fake binary to get stable archive checksums.txt. 27 | -- dist/hugo/v1.2.0/builds/main/base/linux/amd64/hugo -- 28 | linux-amd64 29 | 30 | 31 | # Expected output 32 | -- expected/dist/myrelease/checksums.txt -- 33 | b5bdae6077aadd1c9fccb2ebf25a5305213e4b460c6827277590c8564a231f4a hugo_1.2.0_linux-64bit.tar.gz 34 | b5bdae6077aadd1c9fccb2ebf25a5305213e4b460c6827277590c8564a231f4a hugo_1.2.0_linux-amd64-alias.tar.gz 35 | -- hugoreleaser.yaml -- 36 | project: hugo 37 | archive_alias_replacements: 38 | linux-64bit: linux-amd64-alias 39 | release_settings: 40 | type: github 41 | repository: hugoreleaser 42 | repository_owner: bep 43 | draft: true 44 | release_notes_settings: 45 | filename: temp/my-release-notes.md 46 | build_settings: 47 | binary: hugo 48 | flags: 49 | - -trimpath 50 | archive_settings: 51 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 52 | type: 53 | format: rename 54 | extension: .tar.gz 55 | replacements: 56 | "386": 32bit 57 | amd64: 64bit 58 | arm64: ARM64 59 | darwin: macOS 60 | windows: Windows 61 | builds: 62 | - path: main/base 63 | build_settings: 64 | env: 65 | - CGO_ENABLED=0 66 | ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio 67 | flags: 68 | - -buildmode 69 | - exe 70 | os: 71 | - goos: linux 72 | archs: 73 | - goarch: amd64 74 | - goarch: arm 75 | archives: 76 | - paths: 77 | - builds/**/{darwin,linux}/amd64 78 | archive_settings: {} 79 | - paths: 80 | - builds/**/windows/* 81 | archive_settings: 82 | type: 83 | format: rename 84 | extension: .zip 85 | releases: 86 | - paths: 87 | - archives/** 88 | path: myrelease 89 | 90 | -- go.mod -- 91 | module foo 92 | -- main.go -- 93 | package main 94 | func main() { 95 | 96 | } 97 | 98 | -------------------------------------------------------------------------------- /testscripts/misc/archive_plugin_deb.txt: -------------------------------------------------------------------------------- 1 | 2 | env GOPATH=$WORK/gopath 3 | 4 | hugoreleaser build -tag v1.2.0 5 | 6 | hugoreleaser archive -tag v1.2.0 7 | 8 | checkfile $WORK/dist/hugo/v1.2.0/archives/linux/amd64/hugo_1.2.0_linux-amd64.deb 9 | 10 | # Test files 11 | -- hugoreleaser.yaml -- 12 | project: hugo 13 | build_settings: 14 | binary: hugo 15 | archive_settings: 16 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 17 | extra_files: 18 | - source_path: README.md 19 | target_path: README.md 20 | - source_path: license.txt 21 | target_path: license.txt 22 | type: 23 | format: _plugin 24 | extension: .deb 25 | builds: 26 | - os: 27 | - goos: linux 28 | archs: 29 | - goarch: amd64 30 | archives: 31 | - paths: 32 | - builds/** 33 | archive_settings: 34 | extra_files: [] 35 | plugin: 36 | id: deb 37 | type: gorun 38 | command: github.com/gohugoio/hugoreleaser-archive-plugins/deb@667fb62d3a6c7740d0e3c69e24b4978adbbe889e 39 | custom_settings: 40 | vendor: gohugo.io 41 | homepage: https://gohugo.io/ 42 | maintainer: Bjørn Erik Pedersen 43 | description: A Fast and Flexible Static Site Generator built with love in GoLang. 44 | license: Apache-2.0 45 | 46 | 47 | -- go.mod -- 48 | module foo 49 | -- main.go -- 50 | package main 51 | func main() { 52 | 53 | } 54 | -- README.md -- 55 | This is readme. 56 | -- license.txt -- 57 | This is license. -------------------------------------------------------------------------------- /testscripts/misc/build_chunks.txt: -------------------------------------------------------------------------------- 1 | 2 | # There are 9 binaries in total. 3 | # These gets chunked into 4 chunks a 3,2,2,2. 4 | hugoreleaser build -tag v1.2.0 -chunk-index 0 -chunks 4 5 | ! stderr . 6 | ! stdout linus|windows 7 | checkfilecount 3 $WORK/dist/hugo/v1.2.0/builds 8 | 9 | hugoreleaser build -tag v1.2.0 -chunk-index 1 -chunks 4 10 | checkfilecount 5 $WORK/dist/hugo/v1.2.0/builds 11 | ! stderr . 12 | 13 | hugoreleaser build -tag v1.2.0 -chunk-index 2 -chunks 4 14 | checkfilecount 7 $WORK/dist/hugo/v1.2.0/builds 15 | 16 | hugoreleaser build -tag v1.2.0 -chunk-index 3 -chunks 4 17 | checkfilecount 9 $WORK/dist/hugo/v1.2.0/builds 18 | 19 | ! stderr . 20 | 21 | 22 | # Test files 23 | -- hugoreleaser.yaml -- 24 | project: hugo 25 | build_settings: 26 | binary: hugo 27 | archive_settings: 28 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 29 | type: 30 | format: tar.gz 31 | extension: .tar.gz 32 | replacements: 33 | "386": 32bit 34 | amd64: 64bit 35 | arm64: ARM64 36 | darwin: macOS 37 | windows: Windows 38 | builds: 39 | - path: main/base 40 | build_settings: 41 | env: 42 | - CGO_ENABLED=0 43 | ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio 44 | flags: 45 | - -buildmode 46 | - exe 47 | os: 48 | - goos: darwin 49 | archs: 50 | - goarch: amd64 51 | - goarch: arm64 52 | - goos: freebsd 53 | archs: 54 | - goarch: amd64 55 | - goarch: arm64 56 | - goos: linux 57 | archs: 58 | - goarch: amd64 59 | - goarch: arm64 60 | - goarch: arm 61 | - goos: windows 62 | build_settings: 63 | binary: hugo.exe 64 | archs: 65 | - goarch: amd64 66 | - goarch: arm64 67 | archives: 68 | - paths: 69 | - builds/**/{darwin,linux}/amd64 70 | archive_settings: {} 71 | - paths: 72 | - builds/**/windows/* 73 | archive_settings: 74 | type: 75 | format: zip 76 | extension: .zip 77 | 78 | 79 | -- go.mod -- 80 | module foo 81 | -- main.go -- 82 | package main 83 | func main() { 84 | 85 | } 86 | -- README.md -- 87 | This is readme. 88 | -- license.txt -- 89 | This is license. -------------------------------------------------------------------------------- /testscripts/misc/build_macos_universal_binary.txt: -------------------------------------------------------------------------------- 1 | 2 | env READMEFILE=README.md 3 | # faketoken is a magic string that will create a FakeClient. 4 | env GITHUB_TOKEN=faketoken 5 | 6 | hugoreleaser all -tag v1.2.0 -commitish main 7 | ! stderr . 8 | 9 | stdout 'Prepared 2 files' 10 | stdout 'Uploading.*darwin-universal' 11 | 12 | # Test files 13 | -- hugoreleaser.yaml -- 14 | project: hugo 15 | build_settings: 16 | binary: hugo 17 | release_settings: 18 | type: github 19 | repository: hugoreleaser 20 | repository_owner: bep 21 | draft: true 22 | archive_settings: 23 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 24 | extra_files: 25 | - source_path: README.md 26 | target_path: README.md 27 | - source_path: license.txt 28 | target_path: license.txt 29 | type: 30 | format: tar.gz 31 | extension: .tar.gz 32 | builds: 33 | - path: mac 34 | os: 35 | - goos: darwin 36 | archs: 37 | - goarch: universal 38 | archives: 39 | - paths: 40 | - builds/mac/** 41 | releases: 42 | - paths: 43 | - archives/** 44 | path: myrelease 45 | 46 | 47 | -- go.mod -- 48 | module foo 49 | -- main.go -- 50 | package main 51 | func main() { 52 | 53 | } 54 | -- README.md -- 55 | This is readme. 56 | -- license.txt -- 57 | This is license. -------------------------------------------------------------------------------- /testscripts/misc/envvars.txt: -------------------------------------------------------------------------------- 1 | 2 | env GITHUB_TOKEN=faketoken 3 | env HUGORELEASER_TAG=v1.2.3 4 | env NAME3_FROM_ENV=name3os 5 | 6 | # This file tests the environemt handling, namely: 7 | # 8 | # * Setting flags (HUGORELEASER_TAG) 9 | # * Setting env in hugoreleaser.env 10 | # * Setting flags in hugoreleaser.env 11 | # * Having the same env in OS as in hugoreleaser.env (OS will win). 12 | # * Do env var replacement in hugoreleaser.toml. 13 | # 14 | 15 | hugoreleaser all 16 | 17 | # Draft is set to false in env. 18 | stdout 'fake.*release.*Draft:false' 19 | stdout 'First Release!' 20 | 21 | ! stderr . 22 | exists $WORK/dist/hugo/v1.2.3/archives/mybuilds/darwin/amd64/name1_darwin_name2-name3os-amd64.zip 23 | 24 | -- hugoreleaser.yaml -- 25 | project: hugo 26 | archive_settings: 27 | name_template: ${NAME1_FROM_ENV}_{{ .Goos 28 | }}_${NAME2_FROM_ENV}-${NAME3_FROM_ENV}-{{ .Goarch }} 29 | type: 30 | format: zip 31 | extension: .zip 32 | release_settings: 33 | type: github 34 | repository: hugoreleaser 35 | repository_owner: bep 36 | name: ${MYPROJECT_RELEASE_NAME} 37 | draft: "${MYPROJECT_RELEASE_DRAFT@U}" 38 | builds: 39 | - path: mybuilds 40 | build_settings: {} 41 | os: 42 | - goos: darwin 43 | archs: 44 | - goarch: amd64 45 | archives: 46 | - paths: 47 | - builds/** 48 | releases: 49 | - paths: 50 | - archives/** 51 | path: myrelease 52 | -- go.mod -- 53 | module foo 54 | -- main.go -- 55 | package main 56 | func main() { 57 | 58 | } 59 | -- hugoreleaser.env -- 60 | HUGORELEASER_COMMITISH=main 61 | MYPROJECT_RELEASE_NAME=First Release! 62 | MYPROJECT_RELEASE_DRAFT=false 63 | NAME1_FROM_ENV=name1 64 | NAME2_FROM_ENV=name2 65 | NAME3_FROM_ENV=name3 66 | -- README.md -- 67 | This is readme. 68 | -- license.txt -- 69 | This is license. 70 | -------------------------------------------------------------------------------- /testscripts/misc/errors-release-duplicate-archive.txt: -------------------------------------------------------------------------------- 1 | 2 | # It's possible (and easy) to create configurations which will produce two archives with the same name. 3 | # That may be relevant with multiple releases, but not within the same. 4 | # We need to detect that and throw an error. 5 | 6 | # Build binaries. 7 | ! hugoreleaser all -tag v1.2.0 -commitish main -try 8 | stderr 'main/darwin/amd64.*main/darwin/arm64.*same archive name "hugoreleaser.tar.gz"' 9 | 10 | 11 | -- hugoreleaser.yaml -- 12 | project: hugoreleaser 13 | build_settings: 14 | binary: hugoreleaser 15 | release_settings: 16 | type: github 17 | repository: hugoreleaser 18 | repository_owner: gohugoio 19 | draft: true 20 | release_notes_settings: 21 | generate: true 22 | archive_settings: 23 | name_template: "{{ .Project }}" 24 | type: 25 | format: tar.gz 26 | extension: .tar.gz 27 | builds: 28 | - path: main 29 | os: 30 | - goos: darwin 31 | archs: 32 | - goarch: amd64 33 | - goarch: arm64 34 | archives: 35 | - paths: 36 | - builds/** 37 | releases: 38 | - paths: 39 | - archives/** 40 | path: myrelease 41 | 42 | -- go.mod -- 43 | module foo 44 | -- main.go -- 45 | package main 46 | func main() { 47 | 48 | } 49 | -------------------------------------------------------------------------------- /testscripts/misc/errors_common.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | ! hugoreleaser build 4 | stderr 'flag -tag is required' 5 | 6 | ! hugoreleaser build -tag v1.2.0 7 | stderr 'error opening config file.*hugoreleaser\.yaml' 8 | 9 | ! hugoreleaser archive -tag v1.2.0 10 | -------------------------------------------------------------------------------- /testscripts/misc/errors_invalid_config.txt: -------------------------------------------------------------------------------- 1 | ! hugoreleaser build -tag foo 2 | stderr 'error decoding config file' 3 | 4 | -- hugoreleaser.yaml -- 5 | foo = bar 6 | -------------------------------------------------------------------------------- /testscripts/misc/flag_quiet.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | hugoreleaser build -tag v1.2.0 -quiet 4 | ! stdout . 5 | 6 | hugoreleaser archive -tag v1.2.0 -quiet 7 | ! stdout . 8 | 9 | # Test files 10 | -- hugoreleaser.yaml -- 11 | project: hugo 12 | build_settings: 13 | binary: hugo 14 | builds: 15 | - os: 16 | - goos: linux 17 | archs: 18 | - goarch: amd64 19 | archives: 20 | - paths: 21 | - builds/** 22 | archive_settings: 23 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 24 | type: 25 | format: tar.gz 26 | extension: .tar.gz 27 | 28 | 29 | -- go.mod -- 30 | module foo 31 | -- main.go -- 32 | package main 33 | func main() { 34 | 35 | } 36 | -- README.md -- 37 | This is readme. 38 | -- license.txt -- 39 | This is license. -------------------------------------------------------------------------------- /testscripts/misc/flag_try.txt: -------------------------------------------------------------------------------- 1 | 2 | hugoreleaser build -tag v1.2.0 -try 3 | ! exists $WORK/dist/hugo/v1.2.0/builds/linux/amd64/hugo 4 | stdout 'Building binary.*hugo' 5 | 6 | hugoreleaser archive -tag v1.2.0 -try 7 | ! exists $WORK/dist/hugo/v1.2.0/archives/linux/amd64/hugo_1.2.0_linux-amd64.tar.gz 8 | stdout 'Archive file.*hugo_1.2.0_linux-amd64.tar.gz' 9 | 10 | hugoreleaser release -tag v1.2.0 -commitish main -try 11 | 12 | # Test files 13 | -- hugoreleaser.yaml -- 14 | project: hugo 15 | release_settings: 16 | type: github 17 | repository: hugoreleaser 18 | repository_owner: bep 19 | draft: true 20 | build_settings: 21 | binary: hugo 22 | builds: 23 | - os: 24 | - goos: linux 25 | archs: 26 | - goarch: amd64 27 | archives: 28 | - paths: 29 | - builds/** 30 | archive_settings: 31 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 32 | type: 33 | format: tar.gz 34 | extension: .tar.gz 35 | releases: 36 | - paths: 37 | - archives/** 38 | path: myrelease 39 | 40 | -- go.mod -- 41 | module foo 42 | -- main.go -- 43 | package main 44 | func main() { 45 | 46 | } 47 | -- README.md -- 48 | This is readme. 49 | -- license.txt -- 50 | This is license. -------------------------------------------------------------------------------- /testscripts/misc/flags_in_env.txt: -------------------------------------------------------------------------------- 1 | hugoreleaser build -tag v0.9.0 2 | ! stderr . 3 | 4 | hugoreleaser archive -tag v0.9.0 5 | ! stderr . 6 | 7 | 8 | checkfile $WORK/dist/hugoreleaser/v0.9.0/archives/main/darwin/amd64/hugoreleaser_0.9.0_darwin-amd64.tar.gz 9 | 10 | 11 | # Check that the flags (e.g. -tag) can be used as environment variables in config. 12 | # The example below isn't the most relalistic, though :-) 13 | 14 | -- hugoreleaser.yaml -- 15 | project: hugoreleaser 16 | build_settings: 17 | binary: hugoreleaser 18 | release_settings: 19 | type: github 20 | repository: hugoreleaser 21 | repository_owner: gohugoio 22 | draft: true 23 | release_notes_settings: 24 | generate: true 25 | archive_settings: 26 | name_template: "{{ .Project }}_{{ `${HUGORELEASER_TAG}` | trimPrefix `v` }}_{{ 27 | .Goos }}-{{ .Goarch }}" 28 | type: 29 | format: tar.gz 30 | extension: .tar.gz 31 | builds: 32 | - path: main 33 | os: 34 | - goos: darwin 35 | archs: 36 | - goarch: amd64 37 | archives: 38 | - paths: 39 | - builds/** 40 | releases: 41 | - paths: 42 | - archives/** 43 | path: myrelease 44 | -- go.mod -- 45 | module foo 46 | -- main.go -- 47 | package main 48 | func main() { 49 | 50 | } 51 | -------------------------------------------------------------------------------- /testscripts/misc/releasenotes-custom-template.txt: -------------------------------------------------------------------------------- 1 | env GITHUB_TOKEN=faketoken 2 | env HUGORELEASER_CHANGELOG_GITREPO=$SOURCE 3 | 4 | # Build binaries. 5 | hugoreleaser all -tag v0.51.0 -commitish main 6 | ! stderr . 7 | 8 | cmp $WORK/dist/hugoreleaser/v0.51.0/releases/myrelease/release-notes.md $WORK/expected/release-notes.md 9 | 10 | # Test files 11 | # Expected release notes 12 | -- mytemplates/custom.txt -- 13 | {{ range .ChangeGroups }}{{ range .Changes }}Subject: {{ .Subject }}, Hash: {{ .Hash }}|{{ end }}{{ end }} 14 | -- expected/release-notes.md -- 15 | Subject: Shuffle chunked builds, Hash: 515615e|Subject: Throw an error on duplicate archive names in a release, Hash: 8b4ede0|Subject: Fix failing tests, Hash: 130ca16| 16 | -- hugoreleaser.yaml -- 17 | project: hugoreleaser 18 | build_settings: 19 | binary: hugoreleaser 20 | release_settings: 21 | type: github 22 | repository: hugoreleaser 23 | repository_owner: gohugoio 24 | draft: true 25 | release_notes_settings: 26 | generate: true 27 | template_filename: mytemplates/custom.txt 28 | archive_settings: 29 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 30 | type: 31 | format: tar.gz 32 | extension: .tar.gz 33 | builds: 34 | - path: main 35 | os: 36 | - goos: darwin 37 | archs: 38 | - goarch: amd64 39 | archives: 40 | - paths: 41 | - builds/** 42 | releases: 43 | - paths: 44 | - archives/** 45 | path: myrelease 46 | 47 | -- go.mod -- 48 | module foo 49 | -- main.go -- 50 | package main 51 | func main() { 52 | 53 | } 54 | -------------------------------------------------------------------------------- /testscripts/misc/releasenotes-short.txt: -------------------------------------------------------------------------------- 1 | env GITHUB_TOKEN=faketoken 2 | env HUGORELEASER_CHANGELOG_GITREPO=$SOURCE 3 | 4 | # Build binaries. 5 | hugoreleaser all -tag v0.53.2 -commitish main 6 | ! stderr . 7 | 8 | cmp $WORK/dist/hugoreleaser/v0.53.2/releases/myrelease/release-notes.md $WORK/expected/release-notes.md 9 | 10 | # Test files 11 | # Expected release notes 12 | -- expected/release-notes.md -- 13 | ## Short release 14 | 15 | * testing: Cosmetic change5 to test patch releases 1a9c566 #30 16 | * testing: Cosmetic change3 to test patch releases 0ae1602 #30 17 | 18 | 19 | -- hugoreleaser.yaml -- 20 | project: hugoreleaser 21 | build_settings: 22 | binary: hugoreleaser 23 | release_settings: 24 | type: github 25 | repository: hugoreleaser 26 | repository_owner: gohugoio 27 | draft: true 28 | release_notes_settings: 29 | generate: true 30 | short_threshold: 10 31 | short_title: Short release 32 | groups: 33 | - regexp: change4 34 | ignore: true 35 | archive_settings: 36 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 37 | type: 38 | format: tar.gz 39 | extension: .tar.gz 40 | builds: 41 | - path: main 42 | os: 43 | - goos: darwin 44 | archs: 45 | - goarch: amd64 46 | archives: 47 | - paths: 48 | - builds/** 49 | releases: 50 | - paths: 51 | - archives/** 52 | path: myrelease 53 | 54 | -- go.mod -- 55 | module foo 56 | -- main.go -- 57 | package main 58 | func main() { 59 | 60 | } 61 | -------------------------------------------------------------------------------- /testscripts/misc/releasenotes.txt: -------------------------------------------------------------------------------- 1 | env GITHUB_TOKEN=faketoken 2 | env HUGORELEASER_CHANGELOG_GITREPO=$SOURCE 3 | 4 | # Build binaries. 5 | hugoreleaser all -tag v0.51.0 -commitish main 6 | ! stderr . 7 | 8 | cmp $WORK/dist/hugoreleaser/v0.51.0/releases/myrelease/release-notes.md $WORK/expected/release-notes.md 9 | 10 | # Test files 11 | # Expected release notes 12 | -- expected/release-notes.md -- 13 | ## First 14 | 15 | * Throw an error on duplicate archive names in a release 8b4ede0 16 | 17 | ## Second 18 | 19 | * Fix failing tests 130ca16 20 | 21 | ## Third 22 | 23 | * Shuffle chunked builds 515615e 24 | 25 | 26 | -- hugoreleaser.yaml -- 27 | project: hugoreleaser 28 | build_settings: 29 | binary: hugoreleaser 30 | release_settings: 31 | type: github 32 | repository: hugoreleaser 33 | repository_owner: gohugoio 34 | draft: true 35 | release_notes_settings: 36 | generate: true 37 | groups: 38 | - title: First 39 | regexp: error 40 | - title: Second 41 | regexp: failing 42 | - title: Third 43 | regexp: chunked 44 | archive_settings: 45 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 46 | type: 47 | format: tar.gz 48 | extension: .tar.gz 49 | builds: 50 | - path: main 51 | os: 52 | - goos: darwin 53 | archs: 54 | - goarch: amd64 55 | archives: 56 | - paths: 57 | - builds/** 58 | releases: 59 | - paths: 60 | - archives/** 61 | path: myrelease 62 | 63 | -- go.mod -- 64 | module foo 65 | -- main.go -- 66 | package main 67 | func main() { 68 | 69 | } 70 | -------------------------------------------------------------------------------- /testscripts/misc/segments.txt: -------------------------------------------------------------------------------- 1 | 2 | env HUGORELEASER_TAG=1.2.0 3 | env HUGORELEASER_COMMITISH=main 4 | # faketoken is a magic string that will create a FakeClient. 5 | env GITHUB_TOKEN=faketoken 6 | 7 | # Build arm* and 386. 8 | hugoreleaser build -paths builds/**/{arm,386}* 9 | ! stdout amd64 10 | stdout arm64 11 | 12 | # Archive freebsd only. 13 | # Archive filter in config is "builds/unix/**". 14 | hugoreleaser archive -paths builds/**/freebsd/{arm,386}* 15 | ! stdout linux 16 | ! stdout amd64 17 | stdout freebsd 18 | 19 | # We have now only freebsd 3 archives. 20 | hugoreleaser release -paths releases/bsd 21 | stdout 'Prepared 3 files' # 2 archives + checksums.txt. 22 | 23 | # Test files 24 | -- hugoreleaser.yaml -- 25 | project: hugo 26 | build_settings: 27 | binary: hugo 28 | release_settings: 29 | type: github 30 | repository: hugoreleaser 31 | repository_owner: bep 32 | draft: true 33 | archive_settings: 34 | name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" 35 | extra_files: 36 | - source_path: README.md 37 | target_path: README.md 38 | - source_path: license.txt 39 | target_path: license.txt 40 | type: 41 | format: tar.gz 42 | extension: .tar.gz 43 | builds: 44 | - path: unix 45 | os: 46 | - goos: freebsd 47 | archs: 48 | - goarch: amd64 49 | - goarch: arm64 50 | - goarch: arm 51 | - goarch: "386" 52 | - goos: linux 53 | archs: 54 | - goarch: amd64 55 | - goarch: arm64 56 | - goarch: arm 57 | - path: win 58 | os: 59 | - goos: windows 60 | archs: 61 | - goarch: amd64 62 | - goarch: arm64 63 | archives: 64 | - paths: 65 | - builds/unix/** 66 | releases: 67 | - paths: 68 | - archives/**/freebsd/arm* 69 | path: bsd 70 | - paths: 71 | - archives/win/** 72 | path: win 73 | 74 | 75 | -- go.mod -- 76 | module foo 77 | -- main.go -- 78 | package main 79 | func main() { 80 | 81 | } 82 | -- README.md -- 83 | This is readme. 84 | -- license.txt -- 85 | This is license. -------------------------------------------------------------------------------- /testscripts/unfinished/noop.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gohugoio/hugoreleaser/289ff54c0cb4ab556836cedab20da380a95edd9e/testscripts/unfinished/noop.txt -------------------------------------------------------------------------------- /watch_testscripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | trap exit SIGINT 4 | 5 | # I use "run tests on save" in my editor. 6 | # Unfortantly, changes to text files does not trigger this. Hence this workaround. 7 | while true; do find . -type f -name "*.txt" -o -type f -name "*.yaml" | entr -pd touch main_test.go; done --------------------------------------------------------------------------------