├── .github ├── CODEOWNERS └── FUNDING.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── NOTICE.md ├── README.md ├── check_command.go ├── check_command_test.go ├── cmd ├── check │ └── check.go ├── in │ └── in.go └── out │ └── out.go ├── fakes └── fake_git_hub.go ├── fmt.go ├── github.go ├── github_graphql.go ├── github_test.go ├── go.mod ├── go.sum ├── in_command.go ├── in_command_test.go ├── metadata.go ├── model.go ├── out_command.go ├── out_command_test.go ├── resource_suite_test.go ├── resources.go ├── tools.go └── versions.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @concourse/maintainers 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [taylorsilva] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG base_image=cgr.dev/chainguard/wolfi-base 2 | ARG builder_image=concourse/golang-builder 3 | 4 | ARG BUILDPLATFORM 5 | FROM --platform=${BUILDPLATFORM} ${builder_image} AS builder 6 | 7 | ARG TARGETOS 8 | ARG TARGETARCH 9 | ENV GOOS=$TARGETOS 10 | ENV GOARCH=$TARGETARCH 11 | 12 | COPY . /src 13 | WORKDIR /src 14 | ENV CGO_ENABLED=0 15 | RUN go mod download 16 | RUN go build -o /assets/out ./cmd/out 17 | RUN go build -o /assets/in ./cmd/in 18 | RUN go build -o /assets/check ./cmd/check 19 | RUN set -e; for pkg in $(go list ./...); do \ 20 | go test -o "/tests/$(basename $pkg).test" -c $pkg; \ 21 | done 22 | 23 | FROM ${base_image} AS resource 24 | COPY --from=builder /assets /opt/resource 25 | 26 | FROM resource AS tests 27 | COPY --from=builder /tests /tests 28 | RUN set -e; for test in /tests/*.test; do \ 29 | $test; \ 30 | done 31 | 32 | FROM resource 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | Copyright 2015-2016 Alex Suraci, Chris Brown, and Pivotal Software, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | 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 distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Releases Resource 2 | 3 | Fetches and creates versioned GitHub resources. 4 | 5 | 6 | Build Status 7 | 8 | 9 | 10 | > If you're seeing rate limits affecting you then please add a token to the source 11 | > configuration. This will increase your number of allowed requests. 12 | 13 | ## Source Configuration 14 | 15 | * `owner`: *Required.* The GitHub user or organization name for the repository 16 | that the releases are in. 17 | 18 | * `repository`: *Required.* The repository name that contains the releases. 19 | 20 | * `access_token`: *Optional.* Used for accessing a release in a private-repo 21 | during an `in` and pushing a release to a repo during an `out`. The access 22 | token you create is only required to have the `repo` or `public_repo` scope. 23 | 24 | * `github_api_url`: *Optional.* If you use a non-public GitHub deployment then 25 | you can set your API URL here. 26 | 27 | * `github_v4_api_url`: *Optional.* If you use a non-public GitHub deployment then 28 | you can set your API URL for graphql calls here. 29 | 30 | * `github_uploads_url`: *Optional.* Some GitHub instances have a separate URL 31 | for uploading. If `github_api_url` is set, this value defaults to the same 32 | value, but if you have your own endpoint, this field will override it. 33 | 34 | * `insecure`: *Optional. Default `false`.* When set to `true`, concourse will allow 35 | insecure connection to your github API. 36 | 37 | * `release`: *Optional. Default `true`.* When set to `true`, `check` detects 38 | final releases and `put` publishes final releases (as opposed to 39 | pre-releases). If `false`, `check` will ignore final releases, and `put` will 40 | publish pre-releases if `pre_release` is set to `true` 41 | 42 | * `pre_release`: *Optional. Default `false`.* When set to `true`, `check` 43 | detects pre-releases, and `put` will produce pre-releases (if `release` is 44 | also set to `false`). If `false`, only non-prerelease releases will be detected 45 | and published. 46 | 47 | **note:** if both `release` and `pre_release` are set to `true`, `put` 48 | produces final releases and `check` detects both pre-releases and releases. In 49 | order to produce pre-releases, you must set `pre_release` to `true` and 50 | `release` to `false`. 51 | **note:** if both `release` and `pre_release` are set to `false`, `put` will 52 | still produce final releases. 53 | **note:** releases must have [semver compliant](https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions) tags to be detected. 54 | 55 | * `drafts`: *Optional. Default `false`.* When set to `true`, `put` produces 56 | drafts and `check` only detects drafts. If `false`, only non-draft releases 57 | will be detected and published. Note that releases must have [semver compliant](https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions) 58 | tags to be detected, even if they're drafts. 59 | 60 | * `semver_constraint`: *Optional.* If set, constrain the returned semver tags according 61 | to a semver constraint, e.g. `"~1.2.x"`, `">= 1.2 < 3.0.0 || >= 4.2.3"`. 62 | Follows the rules outlined in https://github.com/Masterminds/semver#checking-version-constraints. 63 | 64 | * `tag_filter`: *Optional.* If set, override default tag filter regular 65 | expression of `v?([^v].*)`. If the filter includes a capture group, the capture 66 | group is used as the release version; otherwise, the entire matching substring 67 | is used as the version. 68 | 69 | * `order_by`: *Optional. One of [`version`, `time`]. Default `version`.* 70 | Selects whether to order releases by version (as extracted by `tag_filter`) 71 | or by time. See `check` behavior described below for details. 72 | 73 | * `asset_dir`: *Optional. Default `false`.* When set to `true`, downloaded assets 74 | will be created in a separate directory called `assets`. Otherwise, they will be 75 | created in the same directory as the other files. 76 | 77 | ### Example 78 | 79 | ``` yaml 80 | - name: gh-release 81 | type: github-release 82 | source: 83 | owner: concourse 84 | repository: concourse 85 | access_token: abcdef1234567890 86 | ``` 87 | 88 | ``` yaml 89 | - get: gh-release 90 | ``` 91 | 92 | ``` yaml 93 | - put: gh-release 94 | params: 95 | name: path/to/name/file 96 | tag: path/to/tag/file 97 | body: path/to/body/file 98 | globs: 99 | - paths/to/files/to/upload-*.tgz 100 | generate_release_notes: true 101 | ``` 102 | 103 | To get a specific version of a release: 104 | 105 | ``` yaml 106 | - get: gh-release 107 | version: { tag: 'v0.0.1' } 108 | ``` 109 | 110 | To set a custom tag filter: 111 | 112 | ```yaml 113 | - name: gh-release 114 | type: github-release 115 | source: 116 | owner: concourse 117 | repository: concourse 118 | tag_filter: "version-(.*)" 119 | ``` 120 | 121 | ## Behavior 122 | 123 | ### `check`: Check for released versions. 124 | 125 | Lists releases, sorted either by their version or time, depending on the `order_by` source option. 126 | 127 | When sorting by version, the version is extracted from the git tag using the `tag_filter` source option. 128 | Versions are compared using [semver](http://semver.org) semantics if possible. 129 | 130 | When sorting by time and a release is published, it uses the publication time, otherwise it uses the creation time. 131 | 132 | The returned list contains an object of the following format for each release (with timestamp in the RFC3339 format): 133 | 134 | ``` 135 | { 136 | "id": "12345", 137 | "tag": "v1.2.3", 138 | "timestamp": "2006-01-02T15:04:05.999999999Z" 139 | } 140 | ``` 141 | 142 | When `check` is given such an object as the `version` parameter, it returns releases from the specified version or time on. 143 | Otherwise it returns the release with the latest version or time. 144 | 145 | ### `in`: Fetch assets from a release. 146 | 147 | Fetches artifacts from the requested release. If `asset_dir` source param is set to `true`, 148 | artifacts will be created in a subdirectory called `assets`. 149 | 150 | Also creates the following files: 151 | 152 | * `tag` containing the git tag name of the release being fetched. 153 | * `version` containing the version determined by the git tag of the release being fetched. 154 | * `body` containing the body text of the release. 155 | * `timestamp` containing the publish or creation timestamp for the release in RFC 3339 format. 156 | * `commit_sha` containing the commit SHA the tag is pointing to. 157 | * `url` containing the HTMLURL for the release being fetched. 158 | 159 | #### Parameters 160 | 161 | * `globs`: *Optional.* A list of globs for files that will be downloaded from 162 | the release. If not specified, all assets will be fetched. 163 | 164 | * `include_source_tarball`: *Optional.* Enables downloading of the source 165 | artifact tarball for the release as `source.tar.gz`. Defaults to `false`. 166 | 167 | * `include_source_zip`: *Optional.* Enables downloading of the source 168 | artifact zip for the release as `source.zip`. Defaults to `false`. 169 | 170 | ### `out`: Publish a release. 171 | 172 | Given a name specified in `name`, a body specified in `body`, and the tag to use 173 | specified in `tag`, this creates a release on GitHub then uploads the files 174 | matching the patterns in `globs` to the release. 175 | 176 | #### Parameters 177 | 178 | * `name`: *Required.* A path to a file containing the name of the release. 179 | 180 | * `tag`: *Required.* A path to a file containing the name of the Git tag to use 181 | for the release. 182 | 183 | * `tag_prefix`: *Optional.* If specified, the tag read from the file will be 184 | prepended with this string. This is useful for adding v in front of version numbers. 185 | 186 | * `commitish`: *Optional.* A path to a file containing the commitish (SHA, tag, 187 | branch name) that the release should be associated with. 188 | 189 | * `body`: *Optional.* A path to a file containing the body text of the release. 190 | 191 | * `globs`: *Optional.* A list of globs for files that will be uploaded alongside 192 | the created release. 193 | 194 | * `generate_release_notes`: *Optional.* Causes GitHub to autogenerate the release notes 195 | when creating a new release, based on the commits since the last release. 196 | If `body` is specified, the body will be pre-pended to the automatically generated 197 | notes. Has no effect when updating an existing release. Defaults to `false`. 198 | 199 | ## Development 200 | 201 | ### Prerequisites 202 | 203 | * golang is *required* - version 1.15.x is tested; earlier versions may also 204 | work. 205 | * docker is *required* - version 17.06.x is tested; earlier versions may also 206 | work. 207 | 208 | ### Running the tests 209 | 210 | The tests have been embedded with the `Dockerfile`; ensuring that the testing 211 | environment is consistent across any `docker` enabled platform. When the docker 212 | image builds, the test are run inside the docker container, on failure they 213 | will stop the build. 214 | 215 | Run the tests with the following command: 216 | 217 | ```sh 218 | docker build -t github-release-resource --target tests . 219 | ``` 220 | 221 | ### Contributing 222 | 223 | Please make all pull requests to the `master` branch and ensure tests pass 224 | locally. 225 | -------------------------------------------------------------------------------- /check_command.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/Masterminds/semver" 7 | "github.com/cppforlife/go-semi-semantic/version" 8 | "github.com/google/go-github/v66/github" 9 | ) 10 | 11 | type CheckCommand struct { 12 | github GitHub 13 | } 14 | 15 | func NewCheckCommand(github GitHub) *CheckCommand { 16 | return &CheckCommand{ 17 | github: github, 18 | } 19 | } 20 | 21 | func SortByVersion(releases []*github.RepositoryRelease, versionParser *versionParser) { 22 | sort.Slice(releases, func(i, j int) bool { 23 | first, err := version.NewVersionFromString(versionParser.parse(*releases[i].TagName)) 24 | if err != nil { 25 | return true 26 | } 27 | 28 | second, err := version.NewVersionFromString(versionParser.parse(*releases[j].TagName)) 29 | if err != nil { 30 | return false 31 | } 32 | 33 | return first.IsLt(second) 34 | }) 35 | } 36 | 37 | func SortByTimestamp(releases []*github.RepositoryRelease) { 38 | sort.Slice(releases, func(i, j int) bool { 39 | a := releases[i] 40 | b := releases[j] 41 | return getTimestamp(a).Before(getTimestamp(b)) 42 | }) 43 | } 44 | 45 | func (c *CheckCommand) Run(request CheckRequest) ([]Version, error) { 46 | releases, err := c.github.ListReleases() 47 | if err != nil { 48 | return []Version{}, err 49 | } 50 | 51 | if len(releases) == 0 { 52 | return []Version{}, nil 53 | } 54 | 55 | orderByTime := false 56 | if request.Source.OrderBy == "time" { 57 | orderByTime = true 58 | } 59 | 60 | var filteredReleases []*github.RepositoryRelease 61 | 62 | versionParser, err := newVersionParser(request.Source.TagFilter) 63 | if err != nil { 64 | return []Version{}, err 65 | } 66 | 67 | var constraint *semver.Constraints 68 | if request.Source.SemverConstraint != "" { 69 | constraint, err = semver.NewConstraint(request.Source.SemverConstraint) 70 | if err != nil { 71 | return []Version{}, err 72 | } 73 | } 74 | 75 | for _, release := range releases { 76 | if request.Source.Drafts != *release.Draft { 77 | continue 78 | } 79 | 80 | // Should we skip this release 81 | // a- prerelease condition dont match our source config 82 | // b- release condition match prerealse in github since github has true/false to describe release/prerelase 83 | if request.Source.PreRelease != *release.Prerelease && request.Source.Release == *release.Prerelease { 84 | continue 85 | } 86 | 87 | if constraint != nil { 88 | if release.TagName == nil { 89 | // Release has no tag, so certainly isn't a valid semver 90 | continue 91 | } 92 | version, err := semver.NewVersion(versionParser.parse(*release.TagName)) 93 | if err != nil { 94 | // Release is not tagged with a valid semver 95 | continue 96 | } 97 | if !constraint.Check(version) { 98 | // Valid semver, but does not satisfy constraint 99 | continue 100 | } 101 | } 102 | 103 | if orderByTime { 104 | // We won't do anything with the tags, so just make sure the filter matches the tag. 105 | var tag string 106 | if release.TagName != nil { 107 | tag = *release.TagName 108 | } 109 | if !versionParser.re.MatchString(tag) { 110 | continue 111 | } 112 | // We don't expect any releases with a missing (zero) timestamp, 113 | // but we skip those just in case, since the data type includes them 114 | if getTimestamp(release).IsZero() { 115 | continue 116 | } 117 | } else { 118 | // We will sort by versions parsed out of tags, so make sure we parse successfully. 119 | if release.TagName == nil { 120 | continue 121 | } 122 | if _, err := version.NewVersionFromString(versionParser.parse(*release.TagName)); err != nil { 123 | continue 124 | } 125 | } 126 | 127 | filteredReleases = append(filteredReleases, release) 128 | } 129 | 130 | // If there are no valid releases, output an empty list. 131 | 132 | if len(filteredReleases) == 0 { 133 | return []Version{}, nil 134 | } 135 | 136 | // Sort releases by time or by version 137 | 138 | if orderByTime { 139 | SortByTimestamp(filteredReleases) 140 | } else { 141 | SortByVersion(filteredReleases, &versionParser) 142 | } 143 | 144 | // If request has no version, output the latest release 145 | 146 | latestRelease := filteredReleases[len(filteredReleases)-1] 147 | 148 | if (request.Version == Version{}) { 149 | return []Version{ 150 | versionFromRelease(latestRelease), 151 | }, nil 152 | } 153 | 154 | // Find first release equal or later than the current version 155 | 156 | var firstIncludedReleaseIndex int = -1 157 | 158 | if orderByTime { 159 | // Only search if request has a timestamp 160 | if !request.Version.Timestamp.IsZero() { 161 | firstIncludedReleaseIndex = sort.Search(len(filteredReleases), func(i int) bool { 162 | release := filteredReleases[i] 163 | return !getTimestamp(release).Before(request.Version.Timestamp) 164 | }) 165 | } 166 | } else { 167 | requestVersion, err := version.NewVersionFromString(versionParser.parse(request.Version.Tag)) 168 | if err == nil { 169 | firstIncludedReleaseIndex = sort.Search(len(filteredReleases), func(i int) bool { 170 | release := filteredReleases[i] 171 | releaseVersion, err := version.NewVersionFromString(versionParser.parse(*release.TagName)) 172 | if err != nil { 173 | return false 174 | } 175 | return !releaseVersion.IsLt(requestVersion) 176 | }) 177 | } 178 | } 179 | 180 | // Output all releases equal or later than the current version, 181 | // or just the latest release if there are no such releases. 182 | 183 | outputVersions := []Version{} 184 | 185 | if firstIncludedReleaseIndex >= 0 && firstIncludedReleaseIndex < len(filteredReleases) { 186 | // Found first release >= current version, so output this and all the following release versions 187 | for i := firstIncludedReleaseIndex; i < len(filteredReleases); i++ { 188 | outputVersions = append(outputVersions, versionFromRelease(filteredReleases[i])) 189 | } 190 | } else { 191 | // No release >= current version, so output the latest release version 192 | outputVersions = append( 193 | outputVersions, 194 | versionFromRelease(filteredReleases[len(filteredReleases)-1]), 195 | ) 196 | } 197 | 198 | return outputVersions, nil 199 | } 200 | -------------------------------------------------------------------------------- /check_command_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/google/go-github/v66/github" 8 | 9 | resource "github.com/concourse/github-release-resource" 10 | "github.com/concourse/github-release-resource/fakes" 11 | ) 12 | 13 | var _ = Describe("Check Command", func() { 14 | var ( 15 | githubClient *fakes.FakeGitHub 16 | command *resource.CheckCommand 17 | 18 | returnedReleases []*github.RepositoryRelease 19 | ) 20 | 21 | BeforeEach(func() { 22 | githubClient = &fakes.FakeGitHub{} 23 | command = resource.NewCheckCommand(githubClient) 24 | 25 | returnedReleases = []*github.RepositoryRelease{} 26 | }) 27 | 28 | JustBeforeEach(func() { 29 | githubClient.ListReleasesReturns(returnedReleases, nil) 30 | }) 31 | 32 | Context("when this is the first time that the resource has been run", func() { 33 | Context("when there are no releases", func() { 34 | BeforeEach(func() { 35 | returnedReleases = []*github.RepositoryRelease{} 36 | }) 37 | 38 | It("returns no versions", func() { 39 | versions, err := command.Run(resource.CheckRequest{}) 40 | Ω(err).ShouldNot(HaveOccurred()) 41 | Ω(versions).Should(BeEmpty()) 42 | }) 43 | }) 44 | 45 | Context("when there are releases that get filtered out", func() { 46 | BeforeEach(func() { 47 | returnedReleases = []*github.RepositoryRelease{ 48 | newDraftRepositoryRelease(1, "v0.1.4"), 49 | } 50 | }) 51 | 52 | Context("and releases are ordered by version", func() { 53 | It("returns no versions", func() { 54 | versions, err := command.Run(resource.CheckRequest{}) 55 | Ω(err).ShouldNot(HaveOccurred()) 56 | Ω(versions).Should(BeEmpty()) 57 | }) 58 | }) 59 | 60 | Context("and releases are ordered by time", func() { 61 | It("returns no versions", func() { 62 | versions, err := command.Run(resource.CheckRequest{ 63 | Source: resource.Source{OrderBy: "time"}, 64 | }) 65 | Ω(err).ShouldNot(HaveOccurred()) 66 | Ω(versions).Should(BeEmpty()) 67 | }) 68 | }) 69 | }) 70 | 71 | Context("when there are releases", func() { 72 | BeforeEach(func() { 73 | returnedReleases = []*github.RepositoryRelease{ 74 | newRepositoryReleaseWithCreatedTime(1, "v0.4.0", 2), 75 | newRepositoryReleaseWithCreatedTime(2, "v0.1.3", 3), 76 | newRepositoryReleaseWithCreatedTime(3, "v0.1.2", 1), 77 | } 78 | }) 79 | 80 | Context("and releases are ordered by version", func() { 81 | It("outputs the most recent version only", func() { 82 | command := resource.NewCheckCommand(githubClient) 83 | 84 | response, err := command.Run(resource.CheckRequest{}) 85 | Ω(err).ShouldNot(HaveOccurred()) 86 | 87 | Ω(response).Should(HaveLen(1)) 88 | Ω(response[0]).Should(Equal(newVersionWithTimestamp(1, "v0.4.0", 2))) 89 | }) 90 | }) 91 | 92 | Context("and releases are ordered by time", func() { 93 | It("outputs the most recent time only", func() { 94 | command := resource.NewCheckCommand(githubClient) 95 | 96 | response, err := command.Run(resource.CheckRequest{ 97 | Source: resource.Source{OrderBy: "time"}, 98 | }) 99 | Ω(err).ShouldNot(HaveOccurred()) 100 | 101 | Ω(response).Should(HaveLen(1)) 102 | Ω(response[0]).Should(Equal(newVersionWithTimestamp(2, "v0.1.3", 3))) 103 | }) 104 | }) 105 | 106 | Context("when there is a semver constraint", func() { 107 | BeforeEach(func() { 108 | returnedReleases = []*github.RepositoryRelease{ 109 | newRepositoryReleaseWithCreatedTime(1, "v0.4.0", 2), 110 | newRepositoryReleaseWithCreatedTime(2, "0.1.3", 3), 111 | newRepositoryReleaseWithCreatedTime(3, "v0.1.2", 1), 112 | newRepositoryReleaseWithCreatedTime(4, "invalid-semver", 4), 113 | } 114 | }) 115 | 116 | It("keeps only those versions matching the constraint", func() { 117 | command := resource.NewCheckCommand(githubClient) 118 | 119 | response, err := command.Run(resource.CheckRequest{ 120 | Source: resource.Source{SemverConstraint: "0.1.x"}, 121 | }) 122 | Ω(err).ShouldNot(HaveOccurred()) 123 | 124 | Ω(response).Should(HaveLen(1)) 125 | Ω(response[0]).Should(Equal(newVersionWithTimestamp(2, "0.1.3", 3))) 126 | }) 127 | 128 | Context("when there is a custom tag filter", func() { 129 | BeforeEach(func() { 130 | returnedReleases = []*github.RepositoryRelease{ 131 | newRepositoryReleaseWithCreatedTime(1, "foo-0.4.0", 2), 132 | newRepositoryReleaseWithCreatedTime(2, "foo-0.1.3", 3), 133 | newRepositoryReleaseWithCreatedTime(3, "foo-0.1.2", 1), 134 | newRepositoryReleaseWithCreatedTime(4, "0.1.4", 4), 135 | } 136 | }) 137 | 138 | It("uses the filter", func() { 139 | command := resource.NewCheckCommand(githubClient) 140 | 141 | response, err := command.Run(resource.CheckRequest{ 142 | Source: resource.Source{ 143 | SemverConstraint: "0.1.x", 144 | TagFilter: "foo-(.*)", 145 | }, 146 | }) 147 | Ω(err).ShouldNot(HaveOccurred()) 148 | 149 | Ω(response).Should(HaveLen(1)) 150 | Ω(response[0]).Should(Equal(newVersionWithTimestamp(2, "foo-0.1.3", 3))) 151 | }) 152 | }) 153 | }) 154 | }) 155 | }) 156 | 157 | Context("when there are prior versions", func() { 158 | Context("when there are no releases", func() { 159 | BeforeEach(func() { 160 | returnedReleases = []*github.RepositoryRelease{} 161 | }) 162 | 163 | It("returns no versions", func() { 164 | versions, err := command.Run(resource.CheckRequest{}) 165 | Ω(err).ShouldNot(HaveOccurred()) 166 | Ω(versions).Should(BeEmpty()) 167 | }) 168 | }) 169 | 170 | Context("when there are releases", func() { 171 | Context("and there is a custom tag filter", func() { 172 | BeforeEach(func() { 173 | returnedReleases = []*github.RepositoryRelease{ 174 | newRepositoryRelease(1, "package-0.1.4"), 175 | newRepositoryRelease(2, "package-0.4.0"), 176 | newRepositoryRelease(3, "package-0.1.3"), 177 | newRepositoryRelease(4, "package-0.1.2"), 178 | } 179 | }) 180 | 181 | It("returns all of the versions that are newer", func() { 182 | command := resource.NewCheckCommand(githubClient) 183 | 184 | response, err := command.Run(resource.CheckRequest{ 185 | Version: resource.Version{ 186 | Tag: "package-0.1.3", 187 | }, 188 | }) 189 | Ω(err).ShouldNot(HaveOccurred()) 190 | 191 | Ω(response).Should(Equal([]resource.Version{ 192 | {ID: "3", Tag: "package-0.1.3"}, 193 | {ID: "1", Tag: "package-0.1.4"}, 194 | {ID: "2", Tag: "package-0.4.0"}, 195 | })) 196 | }) 197 | }) 198 | 199 | Context("and the releases do not contain a draft release", func() { 200 | BeforeEach(func() { 201 | returnedReleases = []*github.RepositoryRelease{ 202 | newRepositoryRelease(1, "v0.1.4"), 203 | newRepositoryRelease(2, "0.4.0"), 204 | newRepositoryRelease(3, "v0.1.3"), 205 | newRepositoryRelease(4, "0.1.2"), 206 | } 207 | }) 208 | 209 | It("returns the current version if it is also the latest", func() { 210 | command := resource.NewCheckCommand(githubClient) 211 | 212 | response, err := command.Run(resource.CheckRequest{ 213 | Version: resource.Version{ 214 | Tag: "0.4.0", 215 | }, 216 | }) 217 | Ω(err).ShouldNot(HaveOccurred()) 218 | 219 | Ω(response).Should(Equal([]resource.Version{ 220 | {ID: "2", Tag: "0.4.0"}, 221 | })) 222 | }) 223 | 224 | It("returns all of the versions that are newer", func() { 225 | command := resource.NewCheckCommand(githubClient) 226 | 227 | response, err := command.Run(resource.CheckRequest{ 228 | Version: resource.Version{ 229 | Tag: "v0.1.3", 230 | }, 231 | }) 232 | Ω(err).ShouldNot(HaveOccurred()) 233 | 234 | Ω(response).Should(Equal([]resource.Version{ 235 | {ID: "3", Tag: "v0.1.3"}, 236 | {ID: "1", Tag: "v0.1.4"}, 237 | {ID: "2", Tag: "0.4.0"}, 238 | })) 239 | }) 240 | 241 | It("returns all newer versions even when current version not found", func() { 242 | command := resource.NewCheckCommand(githubClient) 243 | 244 | response, err := command.Run(resource.CheckRequest{ 245 | Version: resource.Version{ 246 | Tag: "v0.1.4-beta", 247 | }, 248 | }) 249 | Ω(err).ShouldNot(HaveOccurred()) 250 | 251 | Ω(response).Should(Equal([]resource.Version{ 252 | {ID: "1", Tag: "v0.1.4"}, 253 | {ID: "2", Tag: "0.4.0"}, 254 | })) 255 | }) 256 | 257 | It("returns the latest version if the current version is not found", func() { 258 | command := resource.NewCheckCommand(githubClient) 259 | 260 | response, err := command.Run(resource.CheckRequest{ 261 | Version: resource.Version{ 262 | Tag: "v3.4.5", 263 | }, 264 | }) 265 | Ω(err).ShouldNot(HaveOccurred()) 266 | 267 | Ω(response).Should(Equal([]resource.Version{ 268 | {ID: "2", Tag: "0.4.0"}, 269 | })) 270 | }) 271 | 272 | Context("when there are not-quite-semver versions", func() { 273 | BeforeEach(func() { 274 | returnedReleases = append(returnedReleases, newRepositoryRelease(5, "v1")) 275 | returnedReleases = append(returnedReleases, newRepositoryRelease(6, "v0")) 276 | }) 277 | 278 | It("combines them with the semver versions in a reasonable order", func() { 279 | command := resource.NewCheckCommand(githubClient) 280 | 281 | response, err := command.Run(resource.CheckRequest{ 282 | Version: resource.Version{ 283 | Tag: "v0.1.3", 284 | }, 285 | }) 286 | Ω(err).ShouldNot(HaveOccurred()) 287 | 288 | Ω(response).Should(Equal([]resource.Version{ 289 | {ID: "3", Tag: "v0.1.3"}, 290 | {ID: "1", Tag: "v0.1.4"}, 291 | {ID: "2", Tag: "0.4.0"}, 292 | {ID: "5", Tag: "v1"}, 293 | })) 294 | }) 295 | }) 296 | }) 297 | 298 | Context("and one of the releases is a draft", func() { 299 | BeforeEach(func() { 300 | returnedReleases = []*github.RepositoryRelease{ 301 | newDraftRepositoryRelease(1, "v0.1.4"), 302 | newRepositoryRelease(2, "0.4.0"), 303 | newRepositoryRelease(3, "v0.1.3"), 304 | } 305 | }) 306 | 307 | It("returns all of the versions that are newer, and not a draft", func() { 308 | command := resource.NewCheckCommand(githubClient) 309 | 310 | response, err := command.Run(resource.CheckRequest{ 311 | Version: resource.Version{ 312 | Tag: "v0.1.3", 313 | }, 314 | }) 315 | Ω(err).ShouldNot(HaveOccurred()) 316 | 317 | Ω(response).Should(Equal([]resource.Version{ 318 | {ID: "3", Tag: "v0.1.3"}, 319 | {ID: "2", Tag: "0.4.0"}, 320 | })) 321 | }) 322 | }) 323 | 324 | Context("when pre releases are allowed and releases are not", func() { 325 | Context("and one of the releases is a final and another is a draft", func() { 326 | BeforeEach(func() { 327 | returnedReleases = []*github.RepositoryRelease{ 328 | newDraftRepositoryRelease(1, "v0.1.4"), 329 | newRepositoryRelease(2, "0.4.0"), 330 | newPreReleaseRepositoryRelease(3, "v0.4.1-rc.10"), 331 | newPreReleaseRepositoryRelease(4, "0.4.1-rc.9"), 332 | newPreReleaseRepositoryRelease(5, "v0.4.1-rc.8"), 333 | } 334 | 335 | }) 336 | 337 | It("returns all of the versions that are newer, and only pre relases", func() { 338 | command := resource.NewCheckCommand(githubClient) 339 | 340 | response, err := command.Run(resource.CheckRequest{ 341 | Version: resource.Version{ID: "3", Tag: "0.4.1-rc.9"}, 342 | Source: resource.Source{Drafts: false, PreRelease: true, Release: false}, 343 | }) 344 | Ω(err).ShouldNot(HaveOccurred()) 345 | 346 | Ω(response).Should(Equal([]resource.Version{ 347 | {ID: "4", Tag: "0.4.1-rc.9"}, 348 | {ID: "3", Tag: "v0.4.1-rc.10"}, 349 | })) 350 | }) 351 | 352 | It("returns the latest prerelease version if the current version is not found", func() { 353 | command := resource.NewCheckCommand(githubClient) 354 | 355 | response, err := command.Run(resource.CheckRequest{ 356 | Version: resource.Version{ID: "5"}, 357 | Source: resource.Source{Drafts: false, PreRelease: true, Release: false}, 358 | }) 359 | Ω(err).ShouldNot(HaveOccurred()) 360 | 361 | Ω(response).Should(Equal([]resource.Version{ 362 | {ID: "3", Tag: "v0.4.1-rc.10"}, 363 | })) 364 | }) 365 | }) 366 | 367 | }) 368 | 369 | Context("when releases and pre releases are allowed", func() { 370 | Context("and final release is newer", func() { 371 | BeforeEach(func() { 372 | returnedReleases = []*github.RepositoryRelease{ 373 | newDraftRepositoryRelease(1, "v0.1.4"), 374 | newRepositoryRelease(1, "0.3.9"), 375 | newRepositoryRelease(2, "0.4.0"), 376 | newRepositoryRelease(3, "v0.4.2"), 377 | newPreReleaseRepositoryRelease(4, "v0.4.1-rc.10"), 378 | newPreReleaseRepositoryRelease(5, "0.4.1-rc.9"), 379 | newPreReleaseRepositoryRelease(6, "v0.4.2-rc.1"), 380 | } 381 | 382 | }) 383 | 384 | It("returns all of the versions that are newer, and are release and prerealse", func() { 385 | command := resource.NewCheckCommand(githubClient) 386 | 387 | response, err := command.Run(resource.CheckRequest{ 388 | Version: resource.Version{Tag: "0.4.0"}, 389 | Source: resource.Source{Drafts: false, PreRelease: true, Release: true}, 390 | }) 391 | Ω(err).ShouldNot(HaveOccurred()) 392 | 393 | Ω(response).Should(Equal([]resource.Version{ 394 | {ID: "2", Tag: "0.4.0"}, 395 | {ID: "5", Tag: "0.4.1-rc.9"}, 396 | {ID: "4", Tag: "v0.4.1-rc.10"}, 397 | {ID: "6", Tag: "v0.4.2-rc.1"}, 398 | {ID: "3", Tag: "v0.4.2"}, 399 | })) 400 | }) 401 | 402 | It("returns the latest release version if the current version is not found", func() { 403 | command := resource.NewCheckCommand(githubClient) 404 | 405 | response, err := command.Run(resource.CheckRequest{ 406 | Version: resource.Version{ID: "5"}, 407 | Source: resource.Source{Drafts: false, PreRelease: true, Release: true}, 408 | }) 409 | Ω(err).ShouldNot(HaveOccurred()) 410 | 411 | Ω(response).Should(Equal([]resource.Version{ 412 | {ID: "3", Tag: "v0.4.2"}, 413 | })) 414 | }) 415 | }) 416 | 417 | Context("and prerelease is newer", func() { 418 | BeforeEach(func() { 419 | returnedReleases = []*github.RepositoryRelease{ 420 | newDraftRepositoryRelease(1, "v0.1.4"), 421 | newRepositoryRelease(1, "0.3.9"), 422 | newRepositoryRelease(2, "0.4.0"), 423 | newRepositoryRelease(3, "v0.4.2"), 424 | newPreReleaseRepositoryRelease(4, "v0.4.1-rc.10"), 425 | newPreReleaseRepositoryRelease(5, "0.4.1-rc.9"), 426 | newPreReleaseRepositoryRelease(6, "v0.4.2-rc.1"), 427 | newPreReleaseRepositoryRelease(7, "v0.4.3-rc.1"), 428 | } 429 | 430 | }) 431 | 432 | It("returns all of the versions that are newer, and are release and prerelease", func() { 433 | command := resource.NewCheckCommand(githubClient) 434 | 435 | response, err := command.Run(resource.CheckRequest{ 436 | Version: resource.Version{Tag: "0.4.0"}, 437 | Source: resource.Source{Drafts: false, PreRelease: true, Release: true}, 438 | }) 439 | Ω(err).ShouldNot(HaveOccurred()) 440 | 441 | Ω(response).Should(Equal([]resource.Version{ 442 | {ID: "2", Tag: "0.4.0"}, 443 | {ID: "5", Tag: "0.4.1-rc.9"}, 444 | {ID: "4", Tag: "v0.4.1-rc.10"}, 445 | {ID: "6", Tag: "v0.4.2-rc.1"}, 446 | {ID: "3", Tag: "v0.4.2"}, 447 | {ID: "7", Tag: "v0.4.3-rc.1"}, 448 | })) 449 | }) 450 | 451 | It("returns the latest prerelease version if the current version is not found", func() { 452 | command := resource.NewCheckCommand(githubClient) 453 | 454 | response, err := command.Run(resource.CheckRequest{ 455 | Version: resource.Version{ID: "5"}, 456 | Source: resource.Source{Drafts: false, PreRelease: true, Release: true}, 457 | }) 458 | Ω(err).ShouldNot(HaveOccurred()) 459 | 460 | Ω(response).Should(Equal([]resource.Version{ 461 | {ID: "7", Tag: "v0.4.3-rc.1"}, 462 | })) 463 | }) 464 | }) 465 | 466 | }) 467 | 468 | Context("when draft releases are allowed", func() { 469 | Context("and one of the releases is a final release", func() { 470 | BeforeEach(func() { 471 | returnedReleases = []*github.RepositoryRelease{ 472 | newDraftRepositoryRelease(1, "v0.1.4"), 473 | newDraftRepositoryRelease(2, "v0.1.3"), 474 | newDraftRepositoryRelease(3, "v0.1.1"), 475 | newRepositoryRelease(2, "0.4.0"), 476 | } 477 | }) 478 | 479 | It("returns all of the versions that are newer, and only draft", func() { 480 | command := resource.NewCheckCommand(githubClient) 481 | 482 | response, err := command.Run(resource.CheckRequest{ 483 | Version: resource.Version{Tag: "v0.1.3"}, 484 | Source: resource.Source{Drafts: true}, 485 | }) 486 | Ω(err).ShouldNot(HaveOccurred()) 487 | 488 | Ω(response).Should(Equal([]resource.Version{ 489 | {ID: "2", Tag: "v0.1.3"}, 490 | {ID: "1", Tag: "v0.1.4"}, 491 | })) 492 | }) 493 | 494 | It("returns all newer draft versions even if current version is not found", func() { 495 | command := resource.NewCheckCommand(githubClient) 496 | 497 | response, err := command.Run(resource.CheckRequest{ 498 | Version: resource.Version{Tag: "v0.1.2"}, 499 | Source: resource.Source{Drafts: true}, 500 | }) 501 | Ω(err).ShouldNot(HaveOccurred()) 502 | 503 | Ω(response).Should(Equal([]resource.Version{ 504 | {ID: "2", Tag: "v0.1.3"}, 505 | {ID: "1", Tag: "v0.1.4"}, 506 | })) 507 | }) 508 | }) 509 | 510 | Context("and non-of them are semver", func() { 511 | BeforeEach(func() { 512 | returnedReleases = []*github.RepositoryRelease{ 513 | newDraftRepositoryRelease(1, "abc/d"), 514 | newDraftRepositoryRelease(2, "123*4"), 515 | } 516 | }) 517 | 518 | It("returns all of the releases with semver resources", func() { 519 | command := resource.NewCheckCommand(githubClient) 520 | 521 | response, err := command.Run(resource.CheckRequest{ 522 | Version: resource.Version{}, 523 | Source: resource.Source{Drafts: true}, 524 | }) 525 | Ω(err).ShouldNot(HaveOccurred()) 526 | 527 | Ω(response).Should(Equal([]resource.Version{})) 528 | }) 529 | }) 530 | 531 | Context("and one of the releases is not a versioned draft release", func() { 532 | BeforeEach(func() { 533 | returnedReleases = []*github.RepositoryRelease{ 534 | newDraftRepositoryRelease(1, "v0.1.4"), 535 | newDraftRepositoryRelease(2, ""), 536 | newDraftWithNilTagRepositoryRelease(3), 537 | newDraftRepositoryRelease(4, "asdf@example.com"), 538 | } 539 | }) 540 | 541 | It("returns all of the releases with semver resources", func() { 542 | command := resource.NewCheckCommand(githubClient) 543 | 544 | response, err := command.Run(resource.CheckRequest{ 545 | Version: resource.Version{}, 546 | Source: resource.Source{Drafts: true, PreRelease: false}, 547 | }) 548 | Ω(err).ShouldNot(HaveOccurred()) 549 | 550 | Ω(response).Should(Equal([]resource.Version{ 551 | {ID: "1", Tag: "v0.1.4"}, 552 | })) 553 | }) 554 | }) 555 | }) 556 | 557 | Context("ordered by time", func() { 558 | Context("with created time only", func() { 559 | BeforeEach(func() { 560 | returnedReleases = []*github.RepositoryRelease{ 561 | newRepositoryReleaseWithCreatedTime(1, "v0.1.1", 1), 562 | newRepositoryReleaseWithCreatedTime(2, "v0.2.1", 2), 563 | newRepositoryReleaseWithCreatedTime(3, "v0.1.3", 3), 564 | newRepositoryReleaseWithCreatedTime(4, "v0.1.4", 4), 565 | } 566 | }) 567 | It("returns releases with newer created time", func() { 568 | command := resource.NewCheckCommand(githubClient) 569 | 570 | response, err := command.Run(resource.CheckRequest{ 571 | Version: newVersionWithTimestamp(3, "v0.1.3", 3), 572 | Source: resource.Source{OrderBy: "time"}, 573 | }) 574 | Ω(err).ShouldNot(HaveOccurred()) 575 | 576 | Ω(response).Should(Equal([]resource.Version{ 577 | newVersionWithTimestamp(3, "v0.1.3", 3), 578 | newVersionWithTimestamp(4, "v0.1.4", 4), 579 | })) 580 | }) 581 | }) 582 | Context("with published time only", func() { 583 | BeforeEach(func() { 584 | returnedReleases = []*github.RepositoryRelease{ 585 | newRepositoryReleaseWithPublishedTime(1, "v0.1.1", 1), 586 | newRepositoryReleaseWithPublishedTime(2, "v0.2.1", 2), 587 | newRepositoryReleaseWithPublishedTime(3, "v0.1.3", 3), 588 | newRepositoryReleaseWithPublishedTime(4, "v0.1.4", 4), 589 | } 590 | }) 591 | It("returns releases with newer published time", func() { 592 | command := resource.NewCheckCommand(githubClient) 593 | 594 | response, err := command.Run(resource.CheckRequest{ 595 | Version: newVersionWithTimestamp(3, "v0.1.3", 3), 596 | Source: resource.Source{OrderBy: "time"}, 597 | }) 598 | Ω(err).ShouldNot(HaveOccurred()) 599 | 600 | Ω(response).Should(Equal([]resource.Version{ 601 | newVersionWithTimestamp(3, "v0.1.3", 3), 602 | newVersionWithTimestamp(4, "v0.1.4", 4), 603 | })) 604 | }) 605 | }) 606 | Context("with created and published time", func() { 607 | BeforeEach(func() { 608 | returnedReleases = []*github.RepositoryRelease{ 609 | newRepositoryReleaseWithCreatedAndPublishedTime(1, "v0.1.1", 1, 5), 610 | newRepositoryReleaseWithCreatedAndPublishedTime(2, "v0.2.1", 2, 4), 611 | newRepositoryReleaseWithCreatedAndPublishedTime(3, "v0.1.3", 4, 2), 612 | newRepositoryReleaseWithCreatedAndPublishedTime(4, "v0.1.4", 5, 1), 613 | } 614 | }) 615 | It("returns releases with newer published time", func() { 616 | command := resource.NewCheckCommand(githubClient) 617 | 618 | response, err := command.Run(resource.CheckRequest{ 619 | Version: newVersionWithTimestamp(2, "v0.2.1", 4), 620 | Source: resource.Source{OrderBy: "time"}, 621 | }) 622 | Ω(err).ShouldNot(HaveOccurred()) 623 | 624 | Ω(response).Should(Equal([]resource.Version{ 625 | newVersionWithTimestamp(2, "v0.2.1", 4), 626 | newVersionWithTimestamp(1, "v0.1.1", 5), 627 | })) 628 | }) 629 | It("returns releases with newer published time even when current version not found", func() { 630 | command := resource.NewCheckCommand(githubClient) 631 | 632 | response, err := command.Run(resource.CheckRequest{ 633 | Version: newVersionWithTimestamp(9, "v1.0.0", 3), 634 | Source: resource.Source{OrderBy: "time"}, 635 | }) 636 | Ω(err).ShouldNot(HaveOccurred()) 637 | 638 | Ω(response).Should(Equal([]resource.Version{ 639 | newVersionWithTimestamp(2, "v0.2.1", 4), 640 | newVersionWithTimestamp(1, "v0.1.1", 5), 641 | })) 642 | }) 643 | It("returns release with latest published time when request has no timestamp", func() { 644 | command := resource.NewCheckCommand(githubClient) 645 | 646 | response, err := command.Run(resource.CheckRequest{ 647 | Version: resource.Version{ID: "2", Tag: "v0.2.1"}, 648 | Source: resource.Source{OrderBy: "time"}, 649 | }) 650 | Ω(err).ShouldNot(HaveOccurred()) 651 | 652 | Ω(response).Should(Equal([]resource.Version{ 653 | newVersionWithTimestamp(1, "v0.1.1", 5), 654 | })) 655 | }) 656 | 657 | }) 658 | Context("without time", func() { 659 | BeforeEach(func() { 660 | returnedReleases = []*github.RepositoryRelease{ 661 | newRepositoryRelease(1, "v0.1.1"), 662 | newRepositoryRelease(2, "v0.2.1"), 663 | newRepositoryRelease(3, "v0.1.3"), 664 | newRepositoryRelease(4, "v0.1.4"), 665 | } 666 | }) 667 | It("returns empty list", func() { 668 | command := resource.NewCheckCommand(githubClient) 669 | 670 | response, err := command.Run(resource.CheckRequest{ 671 | Version: newVersionWithTimestamp(2, "v0.2.1", 3), 672 | Source: resource.Source{OrderBy: "time"}, 673 | }) 674 | Ω(err).ShouldNot(HaveOccurred()) 675 | Ω(response).Should(Equal([]resource.Version{})) 676 | }) 677 | }) 678 | }) 679 | }) 680 | }) 681 | }) 682 | -------------------------------------------------------------------------------- /cmd/check/check.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/concourse/github-release-resource" 8 | ) 9 | 10 | func main() { 11 | request := resource.NewCheckRequest() 12 | inputRequest(&request) 13 | 14 | github, err := resource.NewGitHubClient(request.Source) 15 | if err != nil { 16 | resource.Fatal("constructing github client", err) 17 | } 18 | 19 | command := resource.NewCheckCommand(github) 20 | response, err := command.Run(request) 21 | if err != nil { 22 | resource.Fatal("running command", err) 23 | } 24 | 25 | outputResponse(response) 26 | } 27 | 28 | func inputRequest(request *resource.CheckRequest) { 29 | if err := json.NewDecoder(os.Stdin).Decode(request); err != nil { 30 | resource.Fatal("reading request from stdin", err) 31 | } 32 | } 33 | 34 | func outputResponse(response []resource.Version) { 35 | if err := json.NewEncoder(os.Stdout).Encode(response); err != nil { 36 | resource.Fatal("writing response to stdout", err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cmd/in/in.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/concourse/github-release-resource" 8 | ) 9 | 10 | func main() { 11 | if len(os.Args) < 2 { 12 | resource.Sayf("usage: %s \n", os.Args[0]) 13 | os.Exit(1) 14 | } 15 | 16 | request := resource.NewInRequest() 17 | inputRequest(&request) 18 | 19 | destDir := os.Args[1] 20 | 21 | github, err := resource.NewGitHubClient(request.Source) 22 | if err != nil { 23 | resource.Fatal("constructing github client", err) 24 | } 25 | 26 | command := resource.NewInCommand(github, os.Stderr) 27 | response, err := command.Run(destDir, request) 28 | if err != nil { 29 | resource.Fatal("running command", err) 30 | } 31 | 32 | outputResponse(response) 33 | } 34 | 35 | func inputRequest(request *resource.InRequest) { 36 | if err := json.NewDecoder(os.Stdin).Decode(request); err != nil { 37 | resource.Fatal("reading request from stdin", err) 38 | } 39 | } 40 | 41 | func outputResponse(response resource.InResponse) { 42 | if err := json.NewEncoder(os.Stdout).Encode(response); err != nil { 43 | resource.Fatal("writing response to stdout", err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cmd/out/out.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/concourse/github-release-resource" 8 | ) 9 | 10 | func main() { 11 | if len(os.Args) < 2 { 12 | resource.Sayf("usage: %s \n", os.Args[0]) 13 | os.Exit(1) 14 | } 15 | 16 | request := resource.NewOutRequest() 17 | inputRequest(&request) 18 | 19 | sourceDir := os.Args[1] 20 | 21 | github, err := resource.NewGitHubClient(request.Source) 22 | if err != nil { 23 | resource.Fatal("constructing github client", err) 24 | } 25 | 26 | command := resource.NewOutCommand(github, os.Stderr) 27 | response, err := command.Run(sourceDir, request) 28 | if err != nil { 29 | resource.Fatal("running command", err) 30 | } 31 | 32 | outputResponse(response) 33 | } 34 | 35 | func inputRequest(request *resource.OutRequest) { 36 | if err := json.NewDecoder(os.Stdin).Decode(request); err != nil { 37 | resource.Fatal("reading request from stdin", err) 38 | } 39 | } 40 | 41 | func outputResponse(response resource.OutResponse) { 42 | if err := json.NewEncoder(os.Stdout).Encode(response); err != nil { 43 | resource.Fatal("writing response to stdout", err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /fakes/fake_git_hub.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakes 3 | 4 | import ( 5 | "io" 6 | "net/url" 7 | "os" 8 | "sync" 9 | 10 | resource "github.com/concourse/github-release-resource" 11 | "github.com/google/go-github/v66/github" 12 | ) 13 | 14 | type FakeGitHub struct { 15 | CreateReleaseStub func(github.RepositoryRelease) (*github.RepositoryRelease, error) 16 | createReleaseMutex sync.RWMutex 17 | createReleaseArgsForCall []struct { 18 | arg1 github.RepositoryRelease 19 | } 20 | createReleaseReturns struct { 21 | result1 *github.RepositoryRelease 22 | result2 error 23 | } 24 | createReleaseReturnsOnCall map[int]struct { 25 | result1 *github.RepositoryRelease 26 | result2 error 27 | } 28 | DeleteReleaseAssetStub func(github.ReleaseAsset) error 29 | deleteReleaseAssetMutex sync.RWMutex 30 | deleteReleaseAssetArgsForCall []struct { 31 | arg1 github.ReleaseAsset 32 | } 33 | deleteReleaseAssetReturns struct { 34 | result1 error 35 | } 36 | deleteReleaseAssetReturnsOnCall map[int]struct { 37 | result1 error 38 | } 39 | DownloadReleaseAssetStub func(github.ReleaseAsset) (io.ReadCloser, error) 40 | downloadReleaseAssetMutex sync.RWMutex 41 | downloadReleaseAssetArgsForCall []struct { 42 | arg1 github.ReleaseAsset 43 | } 44 | downloadReleaseAssetReturns struct { 45 | result1 io.ReadCloser 46 | result2 error 47 | } 48 | downloadReleaseAssetReturnsOnCall map[int]struct { 49 | result1 io.ReadCloser 50 | result2 error 51 | } 52 | GetReleaseStub func(int) (*github.RepositoryRelease, error) 53 | getReleaseMutex sync.RWMutex 54 | getReleaseArgsForCall []struct { 55 | arg1 int 56 | } 57 | getReleaseReturns struct { 58 | result1 *github.RepositoryRelease 59 | result2 error 60 | } 61 | getReleaseReturnsOnCall map[int]struct { 62 | result1 *github.RepositoryRelease 63 | result2 error 64 | } 65 | GetReleaseByTagStub func(string) (*github.RepositoryRelease, error) 66 | getReleaseByTagMutex sync.RWMutex 67 | getReleaseByTagArgsForCall []struct { 68 | arg1 string 69 | } 70 | getReleaseByTagReturns struct { 71 | result1 *github.RepositoryRelease 72 | result2 error 73 | } 74 | getReleaseByTagReturnsOnCall map[int]struct { 75 | result1 *github.RepositoryRelease 76 | result2 error 77 | } 78 | GetTarballLinkStub func(string) (*url.URL, error) 79 | getTarballLinkMutex sync.RWMutex 80 | getTarballLinkArgsForCall []struct { 81 | arg1 string 82 | } 83 | getTarballLinkReturns struct { 84 | result1 *url.URL 85 | result2 error 86 | } 87 | getTarballLinkReturnsOnCall map[int]struct { 88 | result1 *url.URL 89 | result2 error 90 | } 91 | GetZipballLinkStub func(string) (*url.URL, error) 92 | getZipballLinkMutex sync.RWMutex 93 | getZipballLinkArgsForCall []struct { 94 | arg1 string 95 | } 96 | getZipballLinkReturns struct { 97 | result1 *url.URL 98 | result2 error 99 | } 100 | getZipballLinkReturnsOnCall map[int]struct { 101 | result1 *url.URL 102 | result2 error 103 | } 104 | ListReleaseAssetsStub func(github.RepositoryRelease) ([]*github.ReleaseAsset, error) 105 | listReleaseAssetsMutex sync.RWMutex 106 | listReleaseAssetsArgsForCall []struct { 107 | arg1 github.RepositoryRelease 108 | } 109 | listReleaseAssetsReturns struct { 110 | result1 []*github.ReleaseAsset 111 | result2 error 112 | } 113 | listReleaseAssetsReturnsOnCall map[int]struct { 114 | result1 []*github.ReleaseAsset 115 | result2 error 116 | } 117 | ListReleasesStub func() ([]*github.RepositoryRelease, error) 118 | listReleasesMutex sync.RWMutex 119 | listReleasesArgsForCall []struct { 120 | } 121 | listReleasesReturns struct { 122 | result1 []*github.RepositoryRelease 123 | result2 error 124 | } 125 | listReleasesReturnsOnCall map[int]struct { 126 | result1 []*github.RepositoryRelease 127 | result2 error 128 | } 129 | ResolveTagToCommitSHAStub func(string) (string, error) 130 | resolveTagToCommitSHAMutex sync.RWMutex 131 | resolveTagToCommitSHAArgsForCall []struct { 132 | arg1 string 133 | } 134 | resolveTagToCommitSHAReturns struct { 135 | result1 string 136 | result2 error 137 | } 138 | resolveTagToCommitSHAReturnsOnCall map[int]struct { 139 | result1 string 140 | result2 error 141 | } 142 | UpdateReleaseStub func(github.RepositoryRelease) (*github.RepositoryRelease, error) 143 | updateReleaseMutex sync.RWMutex 144 | updateReleaseArgsForCall []struct { 145 | arg1 github.RepositoryRelease 146 | } 147 | updateReleaseReturns struct { 148 | result1 *github.RepositoryRelease 149 | result2 error 150 | } 151 | updateReleaseReturnsOnCall map[int]struct { 152 | result1 *github.RepositoryRelease 153 | result2 error 154 | } 155 | UploadReleaseAssetStub func(github.RepositoryRelease, string, *os.File) error 156 | uploadReleaseAssetMutex sync.RWMutex 157 | uploadReleaseAssetArgsForCall []struct { 158 | arg1 github.RepositoryRelease 159 | arg2 string 160 | arg3 *os.File 161 | } 162 | uploadReleaseAssetReturns struct { 163 | result1 error 164 | } 165 | uploadReleaseAssetReturnsOnCall map[int]struct { 166 | result1 error 167 | } 168 | invocations map[string][][]interface{} 169 | invocationsMutex sync.RWMutex 170 | } 171 | 172 | func (fake *FakeGitHub) CreateRelease(arg1 github.RepositoryRelease) (*github.RepositoryRelease, error) { 173 | fake.createReleaseMutex.Lock() 174 | ret, specificReturn := fake.createReleaseReturnsOnCall[len(fake.createReleaseArgsForCall)] 175 | fake.createReleaseArgsForCall = append(fake.createReleaseArgsForCall, struct { 176 | arg1 github.RepositoryRelease 177 | }{arg1}) 178 | stub := fake.CreateReleaseStub 179 | fakeReturns := fake.createReleaseReturns 180 | fake.recordInvocation("CreateRelease", []interface{}{arg1}) 181 | fake.createReleaseMutex.Unlock() 182 | if stub != nil { 183 | return stub(arg1) 184 | } 185 | if specificReturn { 186 | return ret.result1, ret.result2 187 | } 188 | return fakeReturns.result1, fakeReturns.result2 189 | } 190 | 191 | func (fake *FakeGitHub) CreateReleaseCallCount() int { 192 | fake.createReleaseMutex.RLock() 193 | defer fake.createReleaseMutex.RUnlock() 194 | return len(fake.createReleaseArgsForCall) 195 | } 196 | 197 | func (fake *FakeGitHub) CreateReleaseCalls(stub func(github.RepositoryRelease) (*github.RepositoryRelease, error)) { 198 | fake.createReleaseMutex.Lock() 199 | defer fake.createReleaseMutex.Unlock() 200 | fake.CreateReleaseStub = stub 201 | } 202 | 203 | func (fake *FakeGitHub) CreateReleaseArgsForCall(i int) github.RepositoryRelease { 204 | fake.createReleaseMutex.RLock() 205 | defer fake.createReleaseMutex.RUnlock() 206 | argsForCall := fake.createReleaseArgsForCall[i] 207 | return argsForCall.arg1 208 | } 209 | 210 | func (fake *FakeGitHub) CreateReleaseReturns(result1 *github.RepositoryRelease, result2 error) { 211 | fake.createReleaseMutex.Lock() 212 | defer fake.createReleaseMutex.Unlock() 213 | fake.CreateReleaseStub = nil 214 | fake.createReleaseReturns = struct { 215 | result1 *github.RepositoryRelease 216 | result2 error 217 | }{result1, result2} 218 | } 219 | 220 | func (fake *FakeGitHub) CreateReleaseReturnsOnCall(i int, result1 *github.RepositoryRelease, result2 error) { 221 | fake.createReleaseMutex.Lock() 222 | defer fake.createReleaseMutex.Unlock() 223 | fake.CreateReleaseStub = nil 224 | if fake.createReleaseReturnsOnCall == nil { 225 | fake.createReleaseReturnsOnCall = make(map[int]struct { 226 | result1 *github.RepositoryRelease 227 | result2 error 228 | }) 229 | } 230 | fake.createReleaseReturnsOnCall[i] = struct { 231 | result1 *github.RepositoryRelease 232 | result2 error 233 | }{result1, result2} 234 | } 235 | 236 | func (fake *FakeGitHub) DeleteReleaseAsset(arg1 github.ReleaseAsset) error { 237 | fake.deleteReleaseAssetMutex.Lock() 238 | ret, specificReturn := fake.deleteReleaseAssetReturnsOnCall[len(fake.deleteReleaseAssetArgsForCall)] 239 | fake.deleteReleaseAssetArgsForCall = append(fake.deleteReleaseAssetArgsForCall, struct { 240 | arg1 github.ReleaseAsset 241 | }{arg1}) 242 | stub := fake.DeleteReleaseAssetStub 243 | fakeReturns := fake.deleteReleaseAssetReturns 244 | fake.recordInvocation("DeleteReleaseAsset", []interface{}{arg1}) 245 | fake.deleteReleaseAssetMutex.Unlock() 246 | if stub != nil { 247 | return stub(arg1) 248 | } 249 | if specificReturn { 250 | return ret.result1 251 | } 252 | return fakeReturns.result1 253 | } 254 | 255 | func (fake *FakeGitHub) DeleteReleaseAssetCallCount() int { 256 | fake.deleteReleaseAssetMutex.RLock() 257 | defer fake.deleteReleaseAssetMutex.RUnlock() 258 | return len(fake.deleteReleaseAssetArgsForCall) 259 | } 260 | 261 | func (fake *FakeGitHub) DeleteReleaseAssetCalls(stub func(github.ReleaseAsset) error) { 262 | fake.deleteReleaseAssetMutex.Lock() 263 | defer fake.deleteReleaseAssetMutex.Unlock() 264 | fake.DeleteReleaseAssetStub = stub 265 | } 266 | 267 | func (fake *FakeGitHub) DeleteReleaseAssetArgsForCall(i int) github.ReleaseAsset { 268 | fake.deleteReleaseAssetMutex.RLock() 269 | defer fake.deleteReleaseAssetMutex.RUnlock() 270 | argsForCall := fake.deleteReleaseAssetArgsForCall[i] 271 | return argsForCall.arg1 272 | } 273 | 274 | func (fake *FakeGitHub) DeleteReleaseAssetReturns(result1 error) { 275 | fake.deleteReleaseAssetMutex.Lock() 276 | defer fake.deleteReleaseAssetMutex.Unlock() 277 | fake.DeleteReleaseAssetStub = nil 278 | fake.deleteReleaseAssetReturns = struct { 279 | result1 error 280 | }{result1} 281 | } 282 | 283 | func (fake *FakeGitHub) DeleteReleaseAssetReturnsOnCall(i int, result1 error) { 284 | fake.deleteReleaseAssetMutex.Lock() 285 | defer fake.deleteReleaseAssetMutex.Unlock() 286 | fake.DeleteReleaseAssetStub = nil 287 | if fake.deleteReleaseAssetReturnsOnCall == nil { 288 | fake.deleteReleaseAssetReturnsOnCall = make(map[int]struct { 289 | result1 error 290 | }) 291 | } 292 | fake.deleteReleaseAssetReturnsOnCall[i] = struct { 293 | result1 error 294 | }{result1} 295 | } 296 | 297 | func (fake *FakeGitHub) DownloadReleaseAsset(arg1 github.ReleaseAsset) (io.ReadCloser, error) { 298 | fake.downloadReleaseAssetMutex.Lock() 299 | ret, specificReturn := fake.downloadReleaseAssetReturnsOnCall[len(fake.downloadReleaseAssetArgsForCall)] 300 | fake.downloadReleaseAssetArgsForCall = append(fake.downloadReleaseAssetArgsForCall, struct { 301 | arg1 github.ReleaseAsset 302 | }{arg1}) 303 | stub := fake.DownloadReleaseAssetStub 304 | fakeReturns := fake.downloadReleaseAssetReturns 305 | fake.recordInvocation("DownloadReleaseAsset", []interface{}{arg1}) 306 | fake.downloadReleaseAssetMutex.Unlock() 307 | if stub != nil { 308 | return stub(arg1) 309 | } 310 | if specificReturn { 311 | return ret.result1, ret.result2 312 | } 313 | return fakeReturns.result1, fakeReturns.result2 314 | } 315 | 316 | func (fake *FakeGitHub) DownloadReleaseAssetCallCount() int { 317 | fake.downloadReleaseAssetMutex.RLock() 318 | defer fake.downloadReleaseAssetMutex.RUnlock() 319 | return len(fake.downloadReleaseAssetArgsForCall) 320 | } 321 | 322 | func (fake *FakeGitHub) DownloadReleaseAssetCalls(stub func(github.ReleaseAsset) (io.ReadCloser, error)) { 323 | fake.downloadReleaseAssetMutex.Lock() 324 | defer fake.downloadReleaseAssetMutex.Unlock() 325 | fake.DownloadReleaseAssetStub = stub 326 | } 327 | 328 | func (fake *FakeGitHub) DownloadReleaseAssetArgsForCall(i int) github.ReleaseAsset { 329 | fake.downloadReleaseAssetMutex.RLock() 330 | defer fake.downloadReleaseAssetMutex.RUnlock() 331 | argsForCall := fake.downloadReleaseAssetArgsForCall[i] 332 | return argsForCall.arg1 333 | } 334 | 335 | func (fake *FakeGitHub) DownloadReleaseAssetReturns(result1 io.ReadCloser, result2 error) { 336 | fake.downloadReleaseAssetMutex.Lock() 337 | defer fake.downloadReleaseAssetMutex.Unlock() 338 | fake.DownloadReleaseAssetStub = nil 339 | fake.downloadReleaseAssetReturns = struct { 340 | result1 io.ReadCloser 341 | result2 error 342 | }{result1, result2} 343 | } 344 | 345 | func (fake *FakeGitHub) DownloadReleaseAssetReturnsOnCall(i int, result1 io.ReadCloser, result2 error) { 346 | fake.downloadReleaseAssetMutex.Lock() 347 | defer fake.downloadReleaseAssetMutex.Unlock() 348 | fake.DownloadReleaseAssetStub = nil 349 | if fake.downloadReleaseAssetReturnsOnCall == nil { 350 | fake.downloadReleaseAssetReturnsOnCall = make(map[int]struct { 351 | result1 io.ReadCloser 352 | result2 error 353 | }) 354 | } 355 | fake.downloadReleaseAssetReturnsOnCall[i] = struct { 356 | result1 io.ReadCloser 357 | result2 error 358 | }{result1, result2} 359 | } 360 | 361 | func (fake *FakeGitHub) GetRelease(arg1 int) (*github.RepositoryRelease, error) { 362 | fake.getReleaseMutex.Lock() 363 | ret, specificReturn := fake.getReleaseReturnsOnCall[len(fake.getReleaseArgsForCall)] 364 | fake.getReleaseArgsForCall = append(fake.getReleaseArgsForCall, struct { 365 | arg1 int 366 | }{arg1}) 367 | stub := fake.GetReleaseStub 368 | fakeReturns := fake.getReleaseReturns 369 | fake.recordInvocation("GetRelease", []interface{}{arg1}) 370 | fake.getReleaseMutex.Unlock() 371 | if stub != nil { 372 | return stub(arg1) 373 | } 374 | if specificReturn { 375 | return ret.result1, ret.result2 376 | } 377 | return fakeReturns.result1, fakeReturns.result2 378 | } 379 | 380 | func (fake *FakeGitHub) GetReleaseCallCount() int { 381 | fake.getReleaseMutex.RLock() 382 | defer fake.getReleaseMutex.RUnlock() 383 | return len(fake.getReleaseArgsForCall) 384 | } 385 | 386 | func (fake *FakeGitHub) GetReleaseCalls(stub func(int) (*github.RepositoryRelease, error)) { 387 | fake.getReleaseMutex.Lock() 388 | defer fake.getReleaseMutex.Unlock() 389 | fake.GetReleaseStub = stub 390 | } 391 | 392 | func (fake *FakeGitHub) GetReleaseArgsForCall(i int) int { 393 | fake.getReleaseMutex.RLock() 394 | defer fake.getReleaseMutex.RUnlock() 395 | argsForCall := fake.getReleaseArgsForCall[i] 396 | return argsForCall.arg1 397 | } 398 | 399 | func (fake *FakeGitHub) GetReleaseReturns(result1 *github.RepositoryRelease, result2 error) { 400 | fake.getReleaseMutex.Lock() 401 | defer fake.getReleaseMutex.Unlock() 402 | fake.GetReleaseStub = nil 403 | fake.getReleaseReturns = struct { 404 | result1 *github.RepositoryRelease 405 | result2 error 406 | }{result1, result2} 407 | } 408 | 409 | func (fake *FakeGitHub) GetReleaseReturnsOnCall(i int, result1 *github.RepositoryRelease, result2 error) { 410 | fake.getReleaseMutex.Lock() 411 | defer fake.getReleaseMutex.Unlock() 412 | fake.GetReleaseStub = nil 413 | if fake.getReleaseReturnsOnCall == nil { 414 | fake.getReleaseReturnsOnCall = make(map[int]struct { 415 | result1 *github.RepositoryRelease 416 | result2 error 417 | }) 418 | } 419 | fake.getReleaseReturnsOnCall[i] = struct { 420 | result1 *github.RepositoryRelease 421 | result2 error 422 | }{result1, result2} 423 | } 424 | 425 | func (fake *FakeGitHub) GetReleaseByTag(arg1 string) (*github.RepositoryRelease, error) { 426 | fake.getReleaseByTagMutex.Lock() 427 | ret, specificReturn := fake.getReleaseByTagReturnsOnCall[len(fake.getReleaseByTagArgsForCall)] 428 | fake.getReleaseByTagArgsForCall = append(fake.getReleaseByTagArgsForCall, struct { 429 | arg1 string 430 | }{arg1}) 431 | stub := fake.GetReleaseByTagStub 432 | fakeReturns := fake.getReleaseByTagReturns 433 | fake.recordInvocation("GetReleaseByTag", []interface{}{arg1}) 434 | fake.getReleaseByTagMutex.Unlock() 435 | if stub != nil { 436 | return stub(arg1) 437 | } 438 | if specificReturn { 439 | return ret.result1, ret.result2 440 | } 441 | return fakeReturns.result1, fakeReturns.result2 442 | } 443 | 444 | func (fake *FakeGitHub) GetReleaseByTagCallCount() int { 445 | fake.getReleaseByTagMutex.RLock() 446 | defer fake.getReleaseByTagMutex.RUnlock() 447 | return len(fake.getReleaseByTagArgsForCall) 448 | } 449 | 450 | func (fake *FakeGitHub) GetReleaseByTagCalls(stub func(string) (*github.RepositoryRelease, error)) { 451 | fake.getReleaseByTagMutex.Lock() 452 | defer fake.getReleaseByTagMutex.Unlock() 453 | fake.GetReleaseByTagStub = stub 454 | } 455 | 456 | func (fake *FakeGitHub) GetReleaseByTagArgsForCall(i int) string { 457 | fake.getReleaseByTagMutex.RLock() 458 | defer fake.getReleaseByTagMutex.RUnlock() 459 | argsForCall := fake.getReleaseByTagArgsForCall[i] 460 | return argsForCall.arg1 461 | } 462 | 463 | func (fake *FakeGitHub) GetReleaseByTagReturns(result1 *github.RepositoryRelease, result2 error) { 464 | fake.getReleaseByTagMutex.Lock() 465 | defer fake.getReleaseByTagMutex.Unlock() 466 | fake.GetReleaseByTagStub = nil 467 | fake.getReleaseByTagReturns = struct { 468 | result1 *github.RepositoryRelease 469 | result2 error 470 | }{result1, result2} 471 | } 472 | 473 | func (fake *FakeGitHub) GetReleaseByTagReturnsOnCall(i int, result1 *github.RepositoryRelease, result2 error) { 474 | fake.getReleaseByTagMutex.Lock() 475 | defer fake.getReleaseByTagMutex.Unlock() 476 | fake.GetReleaseByTagStub = nil 477 | if fake.getReleaseByTagReturnsOnCall == nil { 478 | fake.getReleaseByTagReturnsOnCall = make(map[int]struct { 479 | result1 *github.RepositoryRelease 480 | result2 error 481 | }) 482 | } 483 | fake.getReleaseByTagReturnsOnCall[i] = struct { 484 | result1 *github.RepositoryRelease 485 | result2 error 486 | }{result1, result2} 487 | } 488 | 489 | func (fake *FakeGitHub) GetTarballLink(arg1 string) (*url.URL, error) { 490 | fake.getTarballLinkMutex.Lock() 491 | ret, specificReturn := fake.getTarballLinkReturnsOnCall[len(fake.getTarballLinkArgsForCall)] 492 | fake.getTarballLinkArgsForCall = append(fake.getTarballLinkArgsForCall, struct { 493 | arg1 string 494 | }{arg1}) 495 | stub := fake.GetTarballLinkStub 496 | fakeReturns := fake.getTarballLinkReturns 497 | fake.recordInvocation("GetTarballLink", []interface{}{arg1}) 498 | fake.getTarballLinkMutex.Unlock() 499 | if stub != nil { 500 | return stub(arg1) 501 | } 502 | if specificReturn { 503 | return ret.result1, ret.result2 504 | } 505 | return fakeReturns.result1, fakeReturns.result2 506 | } 507 | 508 | func (fake *FakeGitHub) GetTarballLinkCallCount() int { 509 | fake.getTarballLinkMutex.RLock() 510 | defer fake.getTarballLinkMutex.RUnlock() 511 | return len(fake.getTarballLinkArgsForCall) 512 | } 513 | 514 | func (fake *FakeGitHub) GetTarballLinkCalls(stub func(string) (*url.URL, error)) { 515 | fake.getTarballLinkMutex.Lock() 516 | defer fake.getTarballLinkMutex.Unlock() 517 | fake.GetTarballLinkStub = stub 518 | } 519 | 520 | func (fake *FakeGitHub) GetTarballLinkArgsForCall(i int) string { 521 | fake.getTarballLinkMutex.RLock() 522 | defer fake.getTarballLinkMutex.RUnlock() 523 | argsForCall := fake.getTarballLinkArgsForCall[i] 524 | return argsForCall.arg1 525 | } 526 | 527 | func (fake *FakeGitHub) GetTarballLinkReturns(result1 *url.URL, result2 error) { 528 | fake.getTarballLinkMutex.Lock() 529 | defer fake.getTarballLinkMutex.Unlock() 530 | fake.GetTarballLinkStub = nil 531 | fake.getTarballLinkReturns = struct { 532 | result1 *url.URL 533 | result2 error 534 | }{result1, result2} 535 | } 536 | 537 | func (fake *FakeGitHub) GetTarballLinkReturnsOnCall(i int, result1 *url.URL, result2 error) { 538 | fake.getTarballLinkMutex.Lock() 539 | defer fake.getTarballLinkMutex.Unlock() 540 | fake.GetTarballLinkStub = nil 541 | if fake.getTarballLinkReturnsOnCall == nil { 542 | fake.getTarballLinkReturnsOnCall = make(map[int]struct { 543 | result1 *url.URL 544 | result2 error 545 | }) 546 | } 547 | fake.getTarballLinkReturnsOnCall[i] = struct { 548 | result1 *url.URL 549 | result2 error 550 | }{result1, result2} 551 | } 552 | 553 | func (fake *FakeGitHub) GetZipballLink(arg1 string) (*url.URL, error) { 554 | fake.getZipballLinkMutex.Lock() 555 | ret, specificReturn := fake.getZipballLinkReturnsOnCall[len(fake.getZipballLinkArgsForCall)] 556 | fake.getZipballLinkArgsForCall = append(fake.getZipballLinkArgsForCall, struct { 557 | arg1 string 558 | }{arg1}) 559 | stub := fake.GetZipballLinkStub 560 | fakeReturns := fake.getZipballLinkReturns 561 | fake.recordInvocation("GetZipballLink", []interface{}{arg1}) 562 | fake.getZipballLinkMutex.Unlock() 563 | if stub != nil { 564 | return stub(arg1) 565 | } 566 | if specificReturn { 567 | return ret.result1, ret.result2 568 | } 569 | return fakeReturns.result1, fakeReturns.result2 570 | } 571 | 572 | func (fake *FakeGitHub) GetZipballLinkCallCount() int { 573 | fake.getZipballLinkMutex.RLock() 574 | defer fake.getZipballLinkMutex.RUnlock() 575 | return len(fake.getZipballLinkArgsForCall) 576 | } 577 | 578 | func (fake *FakeGitHub) GetZipballLinkCalls(stub func(string) (*url.URL, error)) { 579 | fake.getZipballLinkMutex.Lock() 580 | defer fake.getZipballLinkMutex.Unlock() 581 | fake.GetZipballLinkStub = stub 582 | } 583 | 584 | func (fake *FakeGitHub) GetZipballLinkArgsForCall(i int) string { 585 | fake.getZipballLinkMutex.RLock() 586 | defer fake.getZipballLinkMutex.RUnlock() 587 | argsForCall := fake.getZipballLinkArgsForCall[i] 588 | return argsForCall.arg1 589 | } 590 | 591 | func (fake *FakeGitHub) GetZipballLinkReturns(result1 *url.URL, result2 error) { 592 | fake.getZipballLinkMutex.Lock() 593 | defer fake.getZipballLinkMutex.Unlock() 594 | fake.GetZipballLinkStub = nil 595 | fake.getZipballLinkReturns = struct { 596 | result1 *url.URL 597 | result2 error 598 | }{result1, result2} 599 | } 600 | 601 | func (fake *FakeGitHub) GetZipballLinkReturnsOnCall(i int, result1 *url.URL, result2 error) { 602 | fake.getZipballLinkMutex.Lock() 603 | defer fake.getZipballLinkMutex.Unlock() 604 | fake.GetZipballLinkStub = nil 605 | if fake.getZipballLinkReturnsOnCall == nil { 606 | fake.getZipballLinkReturnsOnCall = make(map[int]struct { 607 | result1 *url.URL 608 | result2 error 609 | }) 610 | } 611 | fake.getZipballLinkReturnsOnCall[i] = struct { 612 | result1 *url.URL 613 | result2 error 614 | }{result1, result2} 615 | } 616 | 617 | func (fake *FakeGitHub) ListReleaseAssets(arg1 github.RepositoryRelease) ([]*github.ReleaseAsset, error) { 618 | fake.listReleaseAssetsMutex.Lock() 619 | ret, specificReturn := fake.listReleaseAssetsReturnsOnCall[len(fake.listReleaseAssetsArgsForCall)] 620 | fake.listReleaseAssetsArgsForCall = append(fake.listReleaseAssetsArgsForCall, struct { 621 | arg1 github.RepositoryRelease 622 | }{arg1}) 623 | stub := fake.ListReleaseAssetsStub 624 | fakeReturns := fake.listReleaseAssetsReturns 625 | fake.recordInvocation("ListReleaseAssets", []interface{}{arg1}) 626 | fake.listReleaseAssetsMutex.Unlock() 627 | if stub != nil { 628 | return stub(arg1) 629 | } 630 | if specificReturn { 631 | return ret.result1, ret.result2 632 | } 633 | return fakeReturns.result1, fakeReturns.result2 634 | } 635 | 636 | func (fake *FakeGitHub) ListReleaseAssetsCallCount() int { 637 | fake.listReleaseAssetsMutex.RLock() 638 | defer fake.listReleaseAssetsMutex.RUnlock() 639 | return len(fake.listReleaseAssetsArgsForCall) 640 | } 641 | 642 | func (fake *FakeGitHub) ListReleaseAssetsCalls(stub func(github.RepositoryRelease) ([]*github.ReleaseAsset, error)) { 643 | fake.listReleaseAssetsMutex.Lock() 644 | defer fake.listReleaseAssetsMutex.Unlock() 645 | fake.ListReleaseAssetsStub = stub 646 | } 647 | 648 | func (fake *FakeGitHub) ListReleaseAssetsArgsForCall(i int) github.RepositoryRelease { 649 | fake.listReleaseAssetsMutex.RLock() 650 | defer fake.listReleaseAssetsMutex.RUnlock() 651 | argsForCall := fake.listReleaseAssetsArgsForCall[i] 652 | return argsForCall.arg1 653 | } 654 | 655 | func (fake *FakeGitHub) ListReleaseAssetsReturns(result1 []*github.ReleaseAsset, result2 error) { 656 | fake.listReleaseAssetsMutex.Lock() 657 | defer fake.listReleaseAssetsMutex.Unlock() 658 | fake.ListReleaseAssetsStub = nil 659 | fake.listReleaseAssetsReturns = struct { 660 | result1 []*github.ReleaseAsset 661 | result2 error 662 | }{result1, result2} 663 | } 664 | 665 | func (fake *FakeGitHub) ListReleaseAssetsReturnsOnCall(i int, result1 []*github.ReleaseAsset, result2 error) { 666 | fake.listReleaseAssetsMutex.Lock() 667 | defer fake.listReleaseAssetsMutex.Unlock() 668 | fake.ListReleaseAssetsStub = nil 669 | if fake.listReleaseAssetsReturnsOnCall == nil { 670 | fake.listReleaseAssetsReturnsOnCall = make(map[int]struct { 671 | result1 []*github.ReleaseAsset 672 | result2 error 673 | }) 674 | } 675 | fake.listReleaseAssetsReturnsOnCall[i] = struct { 676 | result1 []*github.ReleaseAsset 677 | result2 error 678 | }{result1, result2} 679 | } 680 | 681 | func (fake *FakeGitHub) ListReleases() ([]*github.RepositoryRelease, error) { 682 | fake.listReleasesMutex.Lock() 683 | ret, specificReturn := fake.listReleasesReturnsOnCall[len(fake.listReleasesArgsForCall)] 684 | fake.listReleasesArgsForCall = append(fake.listReleasesArgsForCall, struct { 685 | }{}) 686 | stub := fake.ListReleasesStub 687 | fakeReturns := fake.listReleasesReturns 688 | fake.recordInvocation("ListReleases", []interface{}{}) 689 | fake.listReleasesMutex.Unlock() 690 | if stub != nil { 691 | return stub() 692 | } 693 | if specificReturn { 694 | return ret.result1, ret.result2 695 | } 696 | return fakeReturns.result1, fakeReturns.result2 697 | } 698 | 699 | func (fake *FakeGitHub) ListReleasesCallCount() int { 700 | fake.listReleasesMutex.RLock() 701 | defer fake.listReleasesMutex.RUnlock() 702 | return len(fake.listReleasesArgsForCall) 703 | } 704 | 705 | func (fake *FakeGitHub) ListReleasesCalls(stub func() ([]*github.RepositoryRelease, error)) { 706 | fake.listReleasesMutex.Lock() 707 | defer fake.listReleasesMutex.Unlock() 708 | fake.ListReleasesStub = stub 709 | } 710 | 711 | func (fake *FakeGitHub) ListReleasesReturns(result1 []*github.RepositoryRelease, result2 error) { 712 | fake.listReleasesMutex.Lock() 713 | defer fake.listReleasesMutex.Unlock() 714 | fake.ListReleasesStub = nil 715 | fake.listReleasesReturns = struct { 716 | result1 []*github.RepositoryRelease 717 | result2 error 718 | }{result1, result2} 719 | } 720 | 721 | func (fake *FakeGitHub) ListReleasesReturnsOnCall(i int, result1 []*github.RepositoryRelease, result2 error) { 722 | fake.listReleasesMutex.Lock() 723 | defer fake.listReleasesMutex.Unlock() 724 | fake.ListReleasesStub = nil 725 | if fake.listReleasesReturnsOnCall == nil { 726 | fake.listReleasesReturnsOnCall = make(map[int]struct { 727 | result1 []*github.RepositoryRelease 728 | result2 error 729 | }) 730 | } 731 | fake.listReleasesReturnsOnCall[i] = struct { 732 | result1 []*github.RepositoryRelease 733 | result2 error 734 | }{result1, result2} 735 | } 736 | 737 | func (fake *FakeGitHub) ResolveTagToCommitSHA(arg1 string) (string, error) { 738 | fake.resolveTagToCommitSHAMutex.Lock() 739 | ret, specificReturn := fake.resolveTagToCommitSHAReturnsOnCall[len(fake.resolveTagToCommitSHAArgsForCall)] 740 | fake.resolveTagToCommitSHAArgsForCall = append(fake.resolveTagToCommitSHAArgsForCall, struct { 741 | arg1 string 742 | }{arg1}) 743 | stub := fake.ResolveTagToCommitSHAStub 744 | fakeReturns := fake.resolveTagToCommitSHAReturns 745 | fake.recordInvocation("ResolveTagToCommitSHA", []interface{}{arg1}) 746 | fake.resolveTagToCommitSHAMutex.Unlock() 747 | if stub != nil { 748 | return stub(arg1) 749 | } 750 | if specificReturn { 751 | return ret.result1, ret.result2 752 | } 753 | return fakeReturns.result1, fakeReturns.result2 754 | } 755 | 756 | func (fake *FakeGitHub) ResolveTagToCommitSHACallCount() int { 757 | fake.resolveTagToCommitSHAMutex.RLock() 758 | defer fake.resolveTagToCommitSHAMutex.RUnlock() 759 | return len(fake.resolveTagToCommitSHAArgsForCall) 760 | } 761 | 762 | func (fake *FakeGitHub) ResolveTagToCommitSHACalls(stub func(string) (string, error)) { 763 | fake.resolveTagToCommitSHAMutex.Lock() 764 | defer fake.resolveTagToCommitSHAMutex.Unlock() 765 | fake.ResolveTagToCommitSHAStub = stub 766 | } 767 | 768 | func (fake *FakeGitHub) ResolveTagToCommitSHAArgsForCall(i int) string { 769 | fake.resolveTagToCommitSHAMutex.RLock() 770 | defer fake.resolveTagToCommitSHAMutex.RUnlock() 771 | argsForCall := fake.resolveTagToCommitSHAArgsForCall[i] 772 | return argsForCall.arg1 773 | } 774 | 775 | func (fake *FakeGitHub) ResolveTagToCommitSHAReturns(result1 string, result2 error) { 776 | fake.resolveTagToCommitSHAMutex.Lock() 777 | defer fake.resolveTagToCommitSHAMutex.Unlock() 778 | fake.ResolveTagToCommitSHAStub = nil 779 | fake.resolveTagToCommitSHAReturns = struct { 780 | result1 string 781 | result2 error 782 | }{result1, result2} 783 | } 784 | 785 | func (fake *FakeGitHub) ResolveTagToCommitSHAReturnsOnCall(i int, result1 string, result2 error) { 786 | fake.resolveTagToCommitSHAMutex.Lock() 787 | defer fake.resolveTagToCommitSHAMutex.Unlock() 788 | fake.ResolveTagToCommitSHAStub = nil 789 | if fake.resolveTagToCommitSHAReturnsOnCall == nil { 790 | fake.resolveTagToCommitSHAReturnsOnCall = make(map[int]struct { 791 | result1 string 792 | result2 error 793 | }) 794 | } 795 | fake.resolveTagToCommitSHAReturnsOnCall[i] = struct { 796 | result1 string 797 | result2 error 798 | }{result1, result2} 799 | } 800 | 801 | func (fake *FakeGitHub) UpdateRelease(arg1 github.RepositoryRelease) (*github.RepositoryRelease, error) { 802 | fake.updateReleaseMutex.Lock() 803 | ret, specificReturn := fake.updateReleaseReturnsOnCall[len(fake.updateReleaseArgsForCall)] 804 | fake.updateReleaseArgsForCall = append(fake.updateReleaseArgsForCall, struct { 805 | arg1 github.RepositoryRelease 806 | }{arg1}) 807 | stub := fake.UpdateReleaseStub 808 | fakeReturns := fake.updateReleaseReturns 809 | fake.recordInvocation("UpdateRelease", []interface{}{arg1}) 810 | fake.updateReleaseMutex.Unlock() 811 | if stub != nil { 812 | return stub(arg1) 813 | } 814 | if specificReturn { 815 | return ret.result1, ret.result2 816 | } 817 | return fakeReturns.result1, fakeReturns.result2 818 | } 819 | 820 | func (fake *FakeGitHub) UpdateReleaseCallCount() int { 821 | fake.updateReleaseMutex.RLock() 822 | defer fake.updateReleaseMutex.RUnlock() 823 | return len(fake.updateReleaseArgsForCall) 824 | } 825 | 826 | func (fake *FakeGitHub) UpdateReleaseCalls(stub func(github.RepositoryRelease) (*github.RepositoryRelease, error)) { 827 | fake.updateReleaseMutex.Lock() 828 | defer fake.updateReleaseMutex.Unlock() 829 | fake.UpdateReleaseStub = stub 830 | } 831 | 832 | func (fake *FakeGitHub) UpdateReleaseArgsForCall(i int) github.RepositoryRelease { 833 | fake.updateReleaseMutex.RLock() 834 | defer fake.updateReleaseMutex.RUnlock() 835 | argsForCall := fake.updateReleaseArgsForCall[i] 836 | return argsForCall.arg1 837 | } 838 | 839 | func (fake *FakeGitHub) UpdateReleaseReturns(result1 *github.RepositoryRelease, result2 error) { 840 | fake.updateReleaseMutex.Lock() 841 | defer fake.updateReleaseMutex.Unlock() 842 | fake.UpdateReleaseStub = nil 843 | fake.updateReleaseReturns = struct { 844 | result1 *github.RepositoryRelease 845 | result2 error 846 | }{result1, result2} 847 | } 848 | 849 | func (fake *FakeGitHub) UpdateReleaseReturnsOnCall(i int, result1 *github.RepositoryRelease, result2 error) { 850 | fake.updateReleaseMutex.Lock() 851 | defer fake.updateReleaseMutex.Unlock() 852 | fake.UpdateReleaseStub = nil 853 | if fake.updateReleaseReturnsOnCall == nil { 854 | fake.updateReleaseReturnsOnCall = make(map[int]struct { 855 | result1 *github.RepositoryRelease 856 | result2 error 857 | }) 858 | } 859 | fake.updateReleaseReturnsOnCall[i] = struct { 860 | result1 *github.RepositoryRelease 861 | result2 error 862 | }{result1, result2} 863 | } 864 | 865 | func (fake *FakeGitHub) UploadReleaseAsset(arg1 github.RepositoryRelease, arg2 string, arg3 *os.File) error { 866 | fake.uploadReleaseAssetMutex.Lock() 867 | ret, specificReturn := fake.uploadReleaseAssetReturnsOnCall[len(fake.uploadReleaseAssetArgsForCall)] 868 | fake.uploadReleaseAssetArgsForCall = append(fake.uploadReleaseAssetArgsForCall, struct { 869 | arg1 github.RepositoryRelease 870 | arg2 string 871 | arg3 *os.File 872 | }{arg1, arg2, arg3}) 873 | stub := fake.UploadReleaseAssetStub 874 | fakeReturns := fake.uploadReleaseAssetReturns 875 | fake.recordInvocation("UploadReleaseAsset", []interface{}{arg1, arg2, arg3}) 876 | fake.uploadReleaseAssetMutex.Unlock() 877 | if stub != nil { 878 | return stub(arg1, arg2, arg3) 879 | } 880 | if specificReturn { 881 | return ret.result1 882 | } 883 | return fakeReturns.result1 884 | } 885 | 886 | func (fake *FakeGitHub) UploadReleaseAssetCallCount() int { 887 | fake.uploadReleaseAssetMutex.RLock() 888 | defer fake.uploadReleaseAssetMutex.RUnlock() 889 | return len(fake.uploadReleaseAssetArgsForCall) 890 | } 891 | 892 | func (fake *FakeGitHub) UploadReleaseAssetCalls(stub func(github.RepositoryRelease, string, *os.File) error) { 893 | fake.uploadReleaseAssetMutex.Lock() 894 | defer fake.uploadReleaseAssetMutex.Unlock() 895 | fake.UploadReleaseAssetStub = stub 896 | } 897 | 898 | func (fake *FakeGitHub) UploadReleaseAssetArgsForCall(i int) (github.RepositoryRelease, string, *os.File) { 899 | fake.uploadReleaseAssetMutex.RLock() 900 | defer fake.uploadReleaseAssetMutex.RUnlock() 901 | argsForCall := fake.uploadReleaseAssetArgsForCall[i] 902 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 903 | } 904 | 905 | func (fake *FakeGitHub) UploadReleaseAssetReturns(result1 error) { 906 | fake.uploadReleaseAssetMutex.Lock() 907 | defer fake.uploadReleaseAssetMutex.Unlock() 908 | fake.UploadReleaseAssetStub = nil 909 | fake.uploadReleaseAssetReturns = struct { 910 | result1 error 911 | }{result1} 912 | } 913 | 914 | func (fake *FakeGitHub) UploadReleaseAssetReturnsOnCall(i int, result1 error) { 915 | fake.uploadReleaseAssetMutex.Lock() 916 | defer fake.uploadReleaseAssetMutex.Unlock() 917 | fake.UploadReleaseAssetStub = nil 918 | if fake.uploadReleaseAssetReturnsOnCall == nil { 919 | fake.uploadReleaseAssetReturnsOnCall = make(map[int]struct { 920 | result1 error 921 | }) 922 | } 923 | fake.uploadReleaseAssetReturnsOnCall[i] = struct { 924 | result1 error 925 | }{result1} 926 | } 927 | 928 | func (fake *FakeGitHub) Invocations() map[string][][]interface{} { 929 | fake.invocationsMutex.RLock() 930 | defer fake.invocationsMutex.RUnlock() 931 | fake.createReleaseMutex.RLock() 932 | defer fake.createReleaseMutex.RUnlock() 933 | fake.deleteReleaseAssetMutex.RLock() 934 | defer fake.deleteReleaseAssetMutex.RUnlock() 935 | fake.downloadReleaseAssetMutex.RLock() 936 | defer fake.downloadReleaseAssetMutex.RUnlock() 937 | fake.getReleaseMutex.RLock() 938 | defer fake.getReleaseMutex.RUnlock() 939 | fake.getReleaseByTagMutex.RLock() 940 | defer fake.getReleaseByTagMutex.RUnlock() 941 | fake.getTarballLinkMutex.RLock() 942 | defer fake.getTarballLinkMutex.RUnlock() 943 | fake.getZipballLinkMutex.RLock() 944 | defer fake.getZipballLinkMutex.RUnlock() 945 | fake.listReleaseAssetsMutex.RLock() 946 | defer fake.listReleaseAssetsMutex.RUnlock() 947 | fake.listReleasesMutex.RLock() 948 | defer fake.listReleasesMutex.RUnlock() 949 | fake.resolveTagToCommitSHAMutex.RLock() 950 | defer fake.resolveTagToCommitSHAMutex.RUnlock() 951 | fake.updateReleaseMutex.RLock() 952 | defer fake.updateReleaseMutex.RUnlock() 953 | fake.uploadReleaseAssetMutex.RLock() 954 | defer fake.uploadReleaseAssetMutex.RUnlock() 955 | copiedInvocations := map[string][][]interface{}{} 956 | for key, value := range fake.invocations { 957 | copiedInvocations[key] = value 958 | } 959 | return copiedInvocations 960 | } 961 | 962 | func (fake *FakeGitHub) recordInvocation(key string, args []interface{}) { 963 | fake.invocationsMutex.Lock() 964 | defer fake.invocationsMutex.Unlock() 965 | if fake.invocations == nil { 966 | fake.invocations = map[string][][]interface{}{} 967 | } 968 | if fake.invocations[key] == nil { 969 | fake.invocations[key] = [][]interface{}{} 970 | } 971 | fake.invocations[key] = append(fake.invocations[key], args) 972 | } 973 | 974 | var _ resource.GitHub = new(FakeGitHub) 975 | -------------------------------------------------------------------------------- /fmt.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/mitchellh/colorstring" 8 | ) 9 | 10 | func Fatal(doing string, err error) { 11 | Sayf(colorstring.Color("[red]error %s: %s\n"), doing, err) 12 | os.Exit(1) 13 | } 14 | 15 | func Sayf(message string, args ...interface{}) { 16 | fmt.Fprintf(os.Stderr, message, args...) 17 | } 18 | -------------------------------------------------------------------------------- /github.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strings" 13 | 14 | "github.com/google/go-github/v66/github" 15 | "github.com/shurcooL/githubv4" 16 | "golang.org/x/oauth2" 17 | ) 18 | 19 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o fakes/fake_git_hub.go . GitHub 20 | type GitHub interface { 21 | ListReleases() ([]*github.RepositoryRelease, error) 22 | GetReleaseByTag(tag string) (*github.RepositoryRelease, error) 23 | GetRelease(id int) (*github.RepositoryRelease, error) 24 | CreateRelease(release github.RepositoryRelease) (*github.RepositoryRelease, error) 25 | UpdateRelease(release github.RepositoryRelease) (*github.RepositoryRelease, error) 26 | 27 | ListReleaseAssets(release github.RepositoryRelease) ([]*github.ReleaseAsset, error) 28 | UploadReleaseAsset(release github.RepositoryRelease, name string, file *os.File) error 29 | DeleteReleaseAsset(asset github.ReleaseAsset) error 30 | DownloadReleaseAsset(asset github.ReleaseAsset) (io.ReadCloser, error) 31 | 32 | GetTarballLink(tag string) (*url.URL, error) 33 | GetZipballLink(tag string) (*url.URL, error) 34 | ResolveTagToCommitSHA(tag string) (string, error) 35 | } 36 | 37 | type GitHubClient struct { 38 | client *github.Client 39 | clientV4 *githubv4.Client 40 | isEnterprise bool 41 | 42 | owner string 43 | repository string 44 | accessToken string 45 | } 46 | 47 | func NewGitHubClient(source Source) (*GitHubClient, error) { 48 | httpClient := &http.Client{} 49 | ctx := context.TODO() 50 | 51 | if source.Insecure { 52 | httpClient.Transport = &http.Transport{ 53 | Proxy: http.ProxyFromEnvironment, 54 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 55 | } 56 | ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) 57 | } 58 | 59 | if source.AccessToken != "" { 60 | var err error 61 | httpClient, err = oauthClient(ctx, source) 62 | if err != nil { 63 | return nil, err 64 | } 65 | } 66 | 67 | client := github.NewClient(httpClient) 68 | 69 | clientV4 := githubv4.NewClient(httpClient) 70 | var isEnterprise bool 71 | 72 | if source.GitHubAPIURL != "" { 73 | var err error 74 | if !strings.HasSuffix(source.GitHubAPIURL, "/") { 75 | source.GitHubAPIURL += "/" 76 | } 77 | client.BaseURL, err = url.Parse(source.GitHubAPIURL) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | client.UploadURL, err = url.Parse(source.GitHubAPIURL) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | var v4URL string 88 | if strings.HasSuffix(source.GitHubAPIURL, "/v3/") { 89 | v4URL = strings.TrimSuffix(source.GitHubAPIURL, "/v3/") + "/graphql" 90 | } else { 91 | v4URL = source.GitHubAPIURL + "graphql" 92 | } 93 | clientV4 = githubv4.NewEnterpriseClient(v4URL, httpClient) 94 | isEnterprise = true 95 | } 96 | 97 | if source.GitHubV4APIURL != "" { 98 | clientV4 = githubv4.NewEnterpriseClient(source.GitHubV4APIURL, httpClient) 99 | isEnterprise = true 100 | } 101 | 102 | if source.GitHubUploadsURL != "" { 103 | var err error 104 | client.UploadURL, err = url.Parse(source.GitHubUploadsURL) 105 | if err != nil { 106 | return nil, err 107 | } 108 | } 109 | 110 | owner := source.Owner 111 | if source.User != "" { 112 | owner = source.User 113 | } 114 | 115 | return &GitHubClient{ 116 | client: client, 117 | clientV4: clientV4, 118 | isEnterprise: isEnterprise, 119 | owner: owner, 120 | repository: source.Repository, 121 | accessToken: source.AccessToken, 122 | }, nil 123 | } 124 | 125 | func (g *GitHubClient) ListReleases() ([]*github.RepositoryRelease, error) { 126 | if g.accessToken != "" { 127 | if g.isEnterprise { 128 | return g.listReleasesV4EnterPrice() 129 | } 130 | return g.listReleasesV4() 131 | } 132 | opt := &github.ListOptions{PerPage: 100} 133 | var allReleases []*github.RepositoryRelease 134 | for { 135 | releases, res, err := g.client.Repositories.ListReleases(context.TODO(), g.owner, g.repository, opt) 136 | if err != nil { 137 | return []*github.RepositoryRelease{}, err 138 | } 139 | allReleases = append(allReleases, releases...) 140 | if res.NextPage == 0 { 141 | err = res.Body.Close() 142 | if err != nil { 143 | return nil, err 144 | } 145 | break 146 | } 147 | opt.Page = res.NextPage 148 | } 149 | 150 | return allReleases, nil 151 | } 152 | 153 | func (g *GitHubClient) GetReleaseByTag(tag string) (*github.RepositoryRelease, error) { 154 | release, res, err := g.client.Repositories.GetReleaseByTag(context.TODO(), g.owner, g.repository, tag) 155 | if err != nil { 156 | return &github.RepositoryRelease{}, err 157 | } 158 | 159 | err = res.Body.Close() 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | return release, nil 165 | } 166 | 167 | func (g *GitHubClient) GetRelease(id int) (*github.RepositoryRelease, error) { 168 | release, res, err := g.client.Repositories.GetRelease(context.TODO(), g.owner, g.repository, int64(id)) 169 | if err != nil { 170 | return &github.RepositoryRelease{}, err 171 | } 172 | 173 | err = res.Body.Close() 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | return release, nil 179 | } 180 | 181 | func (g *GitHubClient) CreateRelease(release github.RepositoryRelease) (*github.RepositoryRelease, error) { 182 | createdRelease, res, err := g.client.Repositories.CreateRelease(context.TODO(), g.owner, g.repository, &release) 183 | if err != nil { 184 | return &github.RepositoryRelease{}, err 185 | } 186 | 187 | err = res.Body.Close() 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | return createdRelease, nil 193 | } 194 | 195 | func (g *GitHubClient) UpdateRelease(release github.RepositoryRelease) (*github.RepositoryRelease, error) { 196 | if release.ID == nil { 197 | return nil, errors.New("release did not have an ID: has it been saved yet?") 198 | } 199 | 200 | updatedRelease, res, err := g.client.Repositories.EditRelease(context.TODO(), g.owner, g.repository, *release.ID, &release) 201 | if err != nil { 202 | return &github.RepositoryRelease{}, err 203 | } 204 | 205 | err = res.Body.Close() 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | return updatedRelease, nil 211 | } 212 | 213 | func (g *GitHubClient) ListReleaseAssets(release github.RepositoryRelease) ([]*github.ReleaseAsset, error) { 214 | opt := &github.ListOptions{PerPage: 100} 215 | var allAssets []*github.ReleaseAsset 216 | for { 217 | assets, res, err := g.client.Repositories.ListReleaseAssets(context.TODO(), g.owner, g.repository, *release.ID, opt) 218 | if err != nil { 219 | return []*github.ReleaseAsset{}, err 220 | } 221 | allAssets = append(allAssets, assets...) 222 | if res.NextPage == 0 { 223 | err = res.Body.Close() 224 | if err != nil { 225 | return nil, err 226 | } 227 | break 228 | } 229 | opt.Page = res.NextPage 230 | 231 | err = res.Body.Close() 232 | if err != nil { 233 | return nil, err 234 | } 235 | break 236 | } 237 | 238 | return allAssets, nil 239 | } 240 | 241 | func (g *GitHubClient) UploadReleaseAsset(release github.RepositoryRelease, name string, file *os.File) error { 242 | _, res, err := g.client.Repositories.UploadReleaseAsset( 243 | context.TODO(), 244 | g.owner, 245 | g.repository, 246 | *release.ID, 247 | &github.UploadOptions{ 248 | Name: name, 249 | }, 250 | file, 251 | ) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | return res.Body.Close() 257 | } 258 | 259 | func (g *GitHubClient) DeleteReleaseAsset(asset github.ReleaseAsset) error { 260 | res, err := g.client.Repositories.DeleteReleaseAsset(context.TODO(), g.owner, g.repository, *asset.ID) 261 | if err != nil { 262 | return err 263 | } 264 | 265 | return res.Body.Close() 266 | } 267 | 268 | func (g *GitHubClient) DownloadReleaseAsset(asset github.ReleaseAsset) (io.ReadCloser, error) { 269 | bodyReader, redirectURL, err := g.client.Repositories.DownloadReleaseAsset(context.TODO(), g.owner, g.repository, *asset.ID, nil) 270 | if err != nil { 271 | return nil, err 272 | } 273 | 274 | if redirectURL == "" { 275 | return bodyReader, err 276 | } 277 | 278 | req, err := g.client.NewRequest("GET", redirectURL, nil) 279 | if err != nil { 280 | return nil, err 281 | } 282 | req.Header.Set("Accept", "application/octet-stream") 283 | if g.accessToken != "" && req.URL.Host == g.client.BaseURL.Host { 284 | req.Header.Set("Authorization", "Bearer "+g.accessToken) 285 | } 286 | 287 | httpClient := &http.Client{} 288 | resp, err := httpClient.Do(req) 289 | if err != nil { 290 | return nil, err 291 | } 292 | 293 | if resp.StatusCode < 200 || resp.StatusCode > 299 { 294 | resp.Body.Close() 295 | return nil, fmt.Errorf("redirect URL %q responded with bad status code: %d", redirectURL, resp.StatusCode) 296 | } 297 | 298 | return resp.Body, nil 299 | } 300 | 301 | func (g *GitHubClient) GetTarballLink(tag string) (*url.URL, error) { 302 | opt := &github.RepositoryContentGetOptions{Ref: tag} 303 | u, res, err := g.client.Repositories.GetArchiveLink(context.TODO(), g.owner, g.repository, github.Tarball, opt, 10) 304 | if err != nil { 305 | return nil, err 306 | } 307 | res.Body.Close() 308 | return u, nil 309 | } 310 | 311 | func (g *GitHubClient) GetZipballLink(tag string) (*url.URL, error) { 312 | opt := &github.RepositoryContentGetOptions{Ref: tag} 313 | u, res, err := g.client.Repositories.GetArchiveLink(context.TODO(), g.owner, g.repository, github.Zipball, opt, 10) 314 | if err != nil { 315 | return nil, err 316 | } 317 | res.Body.Close() 318 | return u, nil 319 | } 320 | 321 | func (g *GitHubClient) ResolveTagToCommitSHA(tagName string) (string, error) { 322 | ref, res, err := g.client.Git.GetRef(context.TODO(), g.owner, g.repository, "tags/"+tagName) 323 | if err != nil { 324 | return "", err 325 | } 326 | 327 | res.Body.Close() 328 | 329 | // Lightweight tag 330 | if *ref.Object.Type == "commit" { 331 | return *ref.Object.SHA, nil 332 | } 333 | 334 | // Fail if we're not pointing to a annotated tag 335 | if *ref.Object.Type != "tag" { 336 | return "", fmt.Errorf("could not resolve tag %q to commit: returned type is not 'commit' or 'tag'", tagName) 337 | } 338 | 339 | // Resolve tag to commit sha 340 | tag, res, err := g.client.Git.GetTag(context.TODO(), g.owner, g.repository, *ref.Object.SHA) 341 | if err != nil { 342 | return "", err 343 | } 344 | 345 | res.Body.Close() 346 | 347 | if *tag.Object.Type != "commit" { 348 | return "", fmt.Errorf("could not resolve tag %q to commit: returned type is not 'commit'", tagName) 349 | } 350 | 351 | return *tag.Object.SHA, nil 352 | } 353 | 354 | func oauthClient(ctx context.Context, source Source) (*http.Client, error) { 355 | ts := oauth2.StaticTokenSource(&oauth2.Token{ 356 | AccessToken: source.AccessToken, 357 | }) 358 | 359 | oauthClient := oauth2.NewClient(ctx, ts) 360 | 361 | githubHTTPClient := &http.Client{ 362 | Transport: oauthClient.Transport, 363 | } 364 | 365 | return githubHTTPClient, nil 366 | } 367 | -------------------------------------------------------------------------------- /github_graphql.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "errors" 7 | "regexp" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/google/go-github/v66/github" 12 | "github.com/shurcooL/githubv4" 13 | ) 14 | 15 | func (g *GitHubClient) listReleasesV4EnterPrice() ([]*github.RepositoryRelease, error) { 16 | if g.clientV4 == nil { 17 | return nil, errors.New("github graphql is not been initialised") 18 | } 19 | var listReleasesEnterprise struct { 20 | Repository struct { 21 | Releases struct { 22 | Edges []struct { 23 | Node struct { 24 | ReleaseObjectEnterprise 25 | } 26 | } `graphql:"edges"` 27 | PageInfo struct { 28 | EndCursor githubv4.String 29 | HasNextPage bool 30 | } `graphql:"pageInfo"` 31 | } `graphql:"releases(first:$releasesCount, after: $releaseCursor, orderBy: {field: CREATED_AT, direction: DESC})"` 32 | } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` 33 | } 34 | 35 | vars := map[string]any{ 36 | "repositoryOwner": githubv4.String(g.owner), 37 | "repositoryName": githubv4.String(g.repository), 38 | "releaseCursor": (*githubv4.String)(nil), 39 | "releasesCount": githubv4.Int(100), 40 | } 41 | 42 | var allReleases []*github.RepositoryRelease 43 | for { 44 | if err := g.clientV4.Query(context.TODO(), &listReleasesEnterprise, vars); err != nil { 45 | return nil, err 46 | } 47 | for _, r := range listReleasesEnterprise.Repository.Releases.Edges { 48 | r := r 49 | publishedAt, _ := time.ParseInLocation(time.RFC3339, r.Node.PublishedAt.Time.Format(time.RFC3339), time.UTC) 50 | createdAt, _ := time.ParseInLocation(time.RFC3339, r.Node.CreatedAt.Time.Format(time.RFC3339), time.UTC) 51 | var releaseID int64 52 | decodedID, err := base64.StdEncoding.DecodeString(r.Node.ID) 53 | if err != nil { 54 | return nil, err 55 | } 56 | re := regexp.MustCompile(`.*[^\d]`) 57 | decodedID = re.ReplaceAll(decodedID, []byte("")) 58 | if string(decodedID) == "" { 59 | return nil, errors.New("bad release id from graph ql api") 60 | } 61 | releaseID, err = strconv.ParseInt(string(decodedID), 10, 64) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | allReleases = append(allReleases, &github.RepositoryRelease{ 67 | ID: &releaseID, 68 | TagName: &r.Node.TagName, 69 | Name: &r.Node.Name, 70 | Prerelease: &r.Node.IsPrerelease, 71 | Draft: &r.Node.IsDraft, 72 | URL: &r.Node.URL, 73 | PublishedAt: &github.Timestamp{Time: publishedAt}, 74 | CreatedAt: &github.Timestamp{Time: createdAt}, 75 | }) 76 | } 77 | if !listReleasesEnterprise.Repository.Releases.PageInfo.HasNextPage { 78 | break 79 | } 80 | vars["releaseCursor"] = listReleasesEnterprise.Repository.Releases.PageInfo.EndCursor 81 | 82 | } 83 | return allReleases, nil 84 | } 85 | 86 | func (g *GitHubClient) listReleasesV4() ([]*github.RepositoryRelease, error) { 87 | if g.clientV4 == nil { 88 | return nil, errors.New("github graphql is not been initialised") 89 | } 90 | var listReleases struct { 91 | Repository struct { 92 | Releases struct { 93 | Edges []struct { 94 | Node struct { 95 | ReleaseObject 96 | } 97 | } `graphql:"edges"` 98 | PageInfo struct { 99 | EndCursor githubv4.String 100 | HasNextPage bool 101 | } `graphql:"pageInfo"` 102 | } `graphql:"releases(first:$releasesCount, after: $releaseCursor, orderBy: {field: CREATED_AT, direction: DESC})"` 103 | } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` 104 | } 105 | 106 | vars := map[string]any{ 107 | "repositoryOwner": githubv4.String(g.owner), 108 | "repositoryName": githubv4.String(g.repository), 109 | "releaseCursor": (*githubv4.String)(nil), 110 | "releasesCount": githubv4.Int(100), 111 | } 112 | 113 | var allReleases []*github.RepositoryRelease 114 | for { 115 | if err := g.clientV4.Query(context.TODO(), &listReleases, vars); err != nil { 116 | return nil, err 117 | } 118 | 119 | for _, r := range listReleases.Repository.Releases.Edges { 120 | r := r 121 | publishedAt, _ := time.ParseInLocation(time.RFC3339, r.Node.PublishedAt.Time.Format(time.RFC3339), time.UTC) 122 | createdAt, _ := time.ParseInLocation(time.RFC3339, r.Node.CreatedAt.Time.Format(time.RFC3339), time.UTC) 123 | var releaseID int64 124 | if r.Node.DatabaseId == 0 { 125 | decodedID, err := base64.StdEncoding.DecodeString(r.Node.ID) 126 | if err != nil { 127 | return nil, err 128 | } 129 | re := regexp.MustCompile(`.*[^\d]`) 130 | decodedID = re.ReplaceAll(decodedID, []byte("")) 131 | if string(decodedID) == "" { 132 | return nil, errors.New("bad release id from graph ql api") 133 | } 134 | releaseID, err = strconv.ParseInt(string(decodedID), 10, 64) 135 | if err != nil { 136 | return nil, err 137 | } 138 | } else { 139 | releaseID = int64(r.Node.DatabaseId) 140 | } 141 | 142 | allReleases = append(allReleases, &github.RepositoryRelease{ 143 | ID: &releaseID, 144 | TagName: &r.Node.TagName, 145 | Name: &r.Node.Name, 146 | Prerelease: &r.Node.IsPrerelease, 147 | Draft: &r.Node.IsDraft, 148 | URL: &r.Node.URL, 149 | PublishedAt: &github.Timestamp{Time: publishedAt}, 150 | CreatedAt: &github.Timestamp{Time: createdAt}, 151 | }) 152 | } 153 | 154 | if !listReleases.Repository.Releases.PageInfo.HasNextPage { 155 | break 156 | } 157 | vars["releaseCursor"] = listReleases.Repository.Releases.PageInfo.EndCursor 158 | } 159 | 160 | return allReleases, nil 161 | } 162 | -------------------------------------------------------------------------------- /github_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | 10 | . "github.com/concourse/github-release-resource" 11 | 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | 15 | "github.com/google/go-github/v66/github" 16 | "github.com/onsi/gomega/ghttp" 17 | ) 18 | 19 | const ( 20 | multiPageRespEnterprise = `{ 21 | "data": { 22 | "repository": { 23 | "releases": { 24 | "edges": [ 25 | { 26 | "node": { 27 | "createdAt": "2010-10-01T00:58:07Z", 28 | "id": "MDc6UmVsZWFzZTMyMDk1MTAz", 29 | "name": "xyz", 30 | "publishedAt": "2010-10-02T15:39:53Z", 31 | "tagName": "xyz", 32 | "url": "https://github.com/xyz/xyz/releases/tag/xyz", 33 | "isDraft": false, 34 | "isPrerelease": false 35 | } 36 | }, 37 | { 38 | "node": { 39 | "createdAt": "2010-08-27T13:55:36Z", 40 | "id": "MDc6UmVsZWFzZTMwMjMwNjU5", 41 | "name": "xyz", 42 | "publishedAt": "2010-08-27T17:18:06Z", 43 | "tagName": "xyz", 44 | "url": "https://github.com/xyz/xyz/releases/tag/xyz", 45 | "isDraft": false, 46 | "isPrerelease": false 47 | } 48 | } 49 | ], 50 | "pageInfo": { 51 | "endCursor": "Y3Vyc29yOnYyOpK5MjAyMC0xMC0wMVQwMjo1ODowNyswMjowMM4B6bt_", 52 | "hasNextPage": true 53 | } 54 | } 55 | } 56 | } 57 | }` 58 | 59 | singlePageRespEnterprise = `{ 60 | "data": { 61 | "repository": { 62 | "releases": { 63 | "edges": [ 64 | { 65 | "node": { 66 | "createdAt": "2010-10-10T01:01:07Z", 67 | "id": "MDc6UmVsZWFzZTMzMjIyMjQz", 68 | "name": "xyq", 69 | "publishedAt": "2010-10-10T15:39:53Z", 70 | "tagName": "xyq", 71 | "url": "https://github.com/xyq/xyq/releases/tag/xyq", 72 | "isDraft": false, 73 | "isPrerelease": false 74 | } 75 | } 76 | ], 77 | "pageInfo": { 78 | "endCursor": "Y3Vyc29yOnYyOpK5MjAyMC0xMC0wMVQwMjo1ODowNyswMjowMM4B6bt_", 79 | "hasNextPage": false 80 | } 81 | } 82 | } 83 | } 84 | }` 85 | invalidPageIdResp = `{ 86 | "data": { 87 | "repository": { 88 | "releases": { 89 | "edges": [ 90 | { 91 | "node": { 92 | "createdAt": "2010-10-10T01:01:07Z", 93 | "id": "MDc6UmVsZWFzZTMzMjZyzzzz", 94 | "databaseId":"3322224a", 95 | "name": "xyq", 96 | "publishedAt": "2010-10-10T15:39:53Z", 97 | "tagName": "xyq", 98 | "url": "https://github.com/xyq/xyq/releases/tag/xyq", 99 | "isDraft": false, 100 | "isPrerelease": false 101 | } 102 | } 103 | ], 104 | "pageInfo": { 105 | "endCursor": "Y3Vyc29yOnYyOpK5MjAyMC0xMC0wMVQwMjo1ODowNyswMjowMM4B6bt_", 106 | "hasNextPage": false 107 | } 108 | } 109 | } 110 | } 111 | }` 112 | rateLimitMessage = `{ 113 | "message": "API rate limit exceeded for 127.0.0.1. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)", 114 | "documentation_url": "https://developer.github.com/v3/#rate-limiting" 115 | }` 116 | ) 117 | 118 | var _ = Describe("GitHub Client", func() { 119 | var server *ghttp.Server 120 | var client *GitHubClient 121 | var source Source 122 | 123 | BeforeEach(func() { 124 | server = ghttp.NewServer() 125 | }) 126 | 127 | JustBeforeEach(func() { 128 | source.GitHubAPIURL = server.URL() + "/" 129 | 130 | var err error 131 | client, err = NewGitHubClient(source) 132 | Ω(err).ShouldNot(HaveOccurred()) 133 | }) 134 | 135 | AfterEach(func() { 136 | server.Close() 137 | }) 138 | 139 | Context("with bad URLs", func() { 140 | BeforeEach(func() { 141 | source.AccessToken = "hello?" 142 | }) 143 | 144 | It("returns an error if the API URL is bad", func() { 145 | source.GitHubAPIURL = ":" 146 | 147 | _, err := NewGitHubClient(source) 148 | Ω(err).Should(HaveOccurred()) 149 | }) 150 | 151 | It("returns an error if the API URL is bad", func() { 152 | source.GitHubUploadsURL = ":" 153 | 154 | _, err := NewGitHubClient(source) 155 | Ω(err).Should(HaveOccurred()) 156 | }) 157 | }) 158 | 159 | Context("with good URLs", func() { 160 | var err error 161 | BeforeEach(func() { 162 | source = Source{ 163 | Owner: "concourse", 164 | Repository: "concourse", 165 | } 166 | }) 167 | Context("given only the v3 API endpoint", func() { 168 | It("should replace v3 with graphql", func() { 169 | server.AppendHandlers( 170 | ghttp.CombineHandlers( 171 | ghttp.VerifyRequest("POST", "/api/graphql"), 172 | ghttp.RespondWith(200, singlePageRespEnterprise), 173 | ), 174 | ) 175 | 176 | source.GitHubAPIURL = server.URL() + "/api/v3" 177 | //setting the access token is how we ensure the v4 client is used 178 | source.AccessToken = "abc123" 179 | client, err = NewGitHubClient(source) 180 | Ω(err).ShouldNot(HaveOccurred()) 181 | 182 | _, err := client.ListReleases() 183 | Ω(err).ShouldNot(HaveOccurred()) 184 | }) 185 | It("should always append graphql", func() { 186 | server.AppendHandlers( 187 | ghttp.CombineHandlers( 188 | ghttp.VerifyRequest("POST", "/api/graphql"), 189 | ghttp.RespondWith(200, singlePageRespEnterprise), 190 | ), 191 | ) 192 | 193 | source.GitHubAPIURL = server.URL() + "/api/" 194 | //setting the access token is how we ensure the v4 client is used 195 | source.AccessToken = "abc123" 196 | client, err = NewGitHubClient(source) 197 | Ω(err).ShouldNot(HaveOccurred()) 198 | 199 | _, err := client.ListReleases() 200 | Ω(err).ShouldNot(HaveOccurred()) 201 | }) 202 | }) 203 | }) 204 | 205 | Context("with an OAuth Token", func() { 206 | BeforeEach(func() { 207 | source = Source{ 208 | Owner: "concourse", 209 | Repository: "concourse", 210 | AccessToken: "abc123", 211 | } 212 | 213 | server.SetAllowUnhandledRequests(true) 214 | server.AppendHandlers( 215 | ghttp.CombineHandlers( 216 | ghttp.VerifyRequest("POST", "/graphql"), 217 | ghttp.RespondWith(200, singlePageRespEnterprise), 218 | ghttp.VerifyHeaderKV("Authorization", "Bearer abc123"), 219 | ), 220 | ) 221 | }) 222 | 223 | It("sends one", func() { 224 | _, err := client.ListReleases() 225 | Ω(err).ShouldNot(HaveOccurred()) 226 | }) 227 | }) 228 | 229 | Context("without an OAuth Token", func() { 230 | BeforeEach(func() { 231 | source = Source{ 232 | Owner: "concourse", 233 | Repository: "concourse", 234 | } 235 | 236 | server.AppendHandlers( 237 | ghttp.CombineHandlers( 238 | ghttp.VerifyRequest("GET", "/repos/concourse/concourse/releases"), 239 | ghttp.RespondWith(200, "[]"), 240 | ghttp.VerifyHeader(http.Header{"Authorization": nil}), 241 | ), 242 | ) 243 | }) 244 | 245 | It("sends one", func() { 246 | _, err := client.ListReleases() 247 | Ω(err).ShouldNot(HaveOccurred()) 248 | }) 249 | }) 250 | 251 | Describe("ListReleases with access token", func() { 252 | BeforeEach(func() { 253 | source = Source{ 254 | Owner: "concourse", 255 | Repository: "concourse", 256 | AccessToken: "test", 257 | } 258 | }) 259 | Context("List graphql releases", func() { 260 | BeforeEach(func() { 261 | server.AppendHandlers( 262 | ghttp.CombineHandlers( 263 | ghttp.VerifyRequest("POST", "/graphql"), 264 | ghttp.RespondWith(200, multiPageRespEnterprise), 265 | ), 266 | ghttp.CombineHandlers( 267 | ghttp.VerifyRequest("POST", "/graphql"), 268 | ghttp.RespondWith(200, singlePageRespEnterprise), 269 | ), 270 | ) 271 | }) 272 | 273 | It("list releases", func() { 274 | releases, err := client.ListReleases() 275 | Ω(err).ShouldNot(HaveOccurred()) 276 | Expect(releases).To(HaveLen(3)) 277 | Expect(server.ReceivedRequests()).To(HaveLen(2)) 278 | Expect(releases).To(Equal([]*github.RepositoryRelease{ 279 | { 280 | TagName: github.String("xyz"), 281 | Name: github.String("xyz"), 282 | Draft: github.Bool(false), 283 | Prerelease: github.Bool(false), 284 | ID: github.Int64(32095103), 285 | CreatedAt: &github.Timestamp{Time: time.Date(2010, time.October, 01, 00, 58, 07, 0, time.UTC)}, 286 | PublishedAt: &github.Timestamp{Time: time.Date(2010, time.October, 02, 15, 39, 53, 0, time.UTC)}, 287 | URL: github.String("https://github.com/xyz/xyz/releases/tag/xyz"), 288 | }, 289 | { 290 | TagName: github.String("xyz"), 291 | Name: github.String("xyz"), 292 | Draft: github.Bool(false), 293 | Prerelease: github.Bool(false), 294 | ID: github.Int64(30230659), 295 | CreatedAt: &github.Timestamp{Time: time.Date(2010, time.August, 27, 13, 55, 36, 0, time.UTC)}, 296 | PublishedAt: &github.Timestamp{Time: time.Date(2010, time.August, 27, 17, 18, 06, 0, time.UTC)}, 297 | URL: github.String("https://github.com/xyz/xyz/releases/tag/xyz"), 298 | }, 299 | { 300 | TagName: github.String("xyq"), 301 | Name: github.String("xyq"), 302 | Draft: github.Bool(false), 303 | Prerelease: github.Bool(false), 304 | ID: github.Int64(33222243), 305 | CreatedAt: &github.Timestamp{Time: time.Date(2010, time.October, 10, 01, 01, 07, 0, time.UTC)}, 306 | PublishedAt: &github.Timestamp{Time: time.Date(2010, time.October, 10, 15, 39, 53, 0, time.UTC)}, 307 | URL: github.String("https://github.com/xyq/xyq/releases/tag/xyq"), 308 | }, 309 | })) 310 | }) 311 | }) 312 | 313 | Context("List graphql releases with bad id", func() { 314 | BeforeEach(func() { 315 | server.SetAllowUnhandledRequests(true) 316 | server.AppendHandlers( 317 | ghttp.CombineHandlers( 318 | ghttp.VerifyRequest("POST", "/graphql"), 319 | ghttp.RespondWith(200, invalidPageIdResp), 320 | )) 321 | }) 322 | It("list releases with incorrect id", func() { 323 | _, err := client.ListReleases() 324 | Ω(err).Should(HaveOccurred()) 325 | }) 326 | }) 327 | }) 328 | 329 | Describe("ListReleases without access token", func() { 330 | BeforeEach(func() { 331 | source = Source{ 332 | Owner: "concourse", 333 | Repository: "concourse", 334 | } 335 | }) 336 | Context("When list of releases return more then 100 items", func() { 337 | Context("List graphql releases", func() { 338 | BeforeEach(func() { 339 | var result []*github.RepositoryRelease 340 | for i := 1; i < 102; i++ { 341 | result = append(result, &github.RepositoryRelease{ID: github.Int64(int64(i))}) 342 | } 343 | server.AppendHandlers( 344 | ghttp.CombineHandlers(ghttp.VerifyRequest("GET", "/repos/concourse/concourse/releases", "per_page=100"), 345 | ghttp.RespondWithJSONEncoded(200, result[:100], http.Header{"Link": []string{`; rel="next"`}}), 346 | ), 347 | ghttp.CombineHandlers(ghttp.VerifyRequest("GET", "/repos/concourse/concourse/releases", "per_page=100&page=2"), 348 | ghttp.RespondWithJSONEncoded(200, result[100:])), 349 | ) 350 | }) 351 | It("list releases", func() { 352 | releases, err := client.ListReleases() 353 | Ω(err).ShouldNot(HaveOccurred()) 354 | Expect(releases).To(HaveLen(101)) 355 | Expect(server.ReceivedRequests()).To(HaveLen(2)) 356 | }) 357 | 358 | }) 359 | }) 360 | }) 361 | 362 | Describe("GetRelease", func() { 363 | BeforeEach(func() { 364 | source = Source{ 365 | Owner: "concourse", 366 | Repository: "concourse", 367 | } 368 | }) 369 | Context("When GitHub's rate limit has been exceeded", func() { 370 | BeforeEach(func() { 371 | rateLimitResponse := rateLimitMessage 372 | 373 | rateLimitHeaders := http.Header(map[string][]string{ 374 | "X-RateLimit-Limit": {"60"}, 375 | "X-RateLimit-Remaining": {"0"}, 376 | "X-RateLimit-Reset": {"1377013266"}, 377 | }) 378 | 379 | server.AppendHandlers( 380 | ghttp.CombineHandlers( 381 | ghttp.VerifyRequest("GET", "/repos/concourse/concourse/releases/20"), 382 | ghttp.RespondWith(403, rateLimitResponse, rateLimitHeaders), 383 | ), 384 | ) 385 | }) 386 | 387 | It("Returns an appropriate error", func() { 388 | _, err := client.GetRelease(20) 389 | Expect(err).ToNot(BeNil()) 390 | Expect(err.Error()).To(ContainSubstring("API rate limit exceeded for 127.0.0.1. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)")) 391 | }) 392 | }) 393 | }) 394 | 395 | Describe("GetReleaseByTag", func() { 396 | BeforeEach(func() { 397 | source = Source{ 398 | Owner: "concourse", 399 | Repository: "concourse", 400 | } 401 | }) 402 | 403 | Context("When GitHub's rate limit has been exceeded", func() { 404 | BeforeEach(func() { 405 | rateLimitResponse := rateLimitMessage 406 | 407 | rateLimitHeaders := http.Header(map[string][]string{ 408 | "X-RateLimit-Limit": {"60"}, 409 | "X-RateLimit-Remaining": {"0"}, 410 | "X-RateLimit-Reset": {"1377013266"}, 411 | }) 412 | 413 | server.AppendHandlers( 414 | ghttp.CombineHandlers( 415 | ghttp.VerifyRequest("GET", "/repos/concourse/concourse/releases/tags/some-tag"), 416 | ghttp.RespondWith(403, rateLimitResponse, rateLimitHeaders), 417 | ), 418 | ) 419 | }) 420 | 421 | It("Returns an appropriate error", func() { 422 | _, err := client.GetReleaseByTag("some-tag") 423 | Expect(err).ToNot(BeNil()) 424 | Expect(err.Error()).To(ContainSubstring("API rate limit exceeded for 127.0.0.1. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)")) 425 | }) 426 | }) 427 | 428 | Context("When GitHub responds successfully", func() { 429 | BeforeEach(func() { 430 | server.AppendHandlers( 431 | ghttp.CombineHandlers( 432 | ghttp.VerifyRequest("GET", "/repos/concourse/concourse/releases/tags/some-tag"), 433 | ghttp.RespondWith(200, `{ "id": 1 }`), 434 | ), 435 | ) 436 | }) 437 | 438 | It("Returns a populated github.RepositoryRelease", func() { 439 | expectedRelease := &github.RepositoryRelease{ 440 | ID: github.Int64(1), 441 | } 442 | 443 | release, err := client.GetReleaseByTag("some-tag") 444 | 445 | Ω(err).ShouldNot(HaveOccurred()) 446 | Expect(release).To(Equal(expectedRelease)) 447 | }) 448 | }) 449 | }) 450 | 451 | Describe("ResolveTagToCommitSHA", func() { 452 | BeforeEach(func() { 453 | source = Source{ 454 | Owner: "concourse", 455 | Repository: "concourse", 456 | } 457 | }) 458 | 459 | Context("When GitHub's rate limit has been exceeded", func() { 460 | BeforeEach(func() { 461 | rateLimitResponse := rateLimitMessage 462 | 463 | rateLimitHeaders := http.Header(map[string][]string{ 464 | "X-RateLimit-Limit": {"60"}, 465 | "X-RateLimit-Remaining": {"0"}, 466 | "X-RateLimit-Reset": {"1377013266"}, 467 | }) 468 | 469 | server.AppendHandlers( 470 | ghttp.CombineHandlers( 471 | ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/ref/tags/some-tag"), 472 | ghttp.RespondWith(403, rateLimitResponse, rateLimitHeaders), 473 | ), 474 | ) 475 | }) 476 | 477 | It("Returns an appropriate error", func() { 478 | _, err := client.ResolveTagToCommitSHA("some-tag") 479 | Expect(err).ToNot(BeNil()) 480 | Expect(err.Error()).To(ContainSubstring("API rate limit exceeded for 127.0.0.1. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)")) 481 | }) 482 | }) 483 | 484 | Context("When GitHub returns a lightweight tag", func() { 485 | BeforeEach(func() { 486 | server.AppendHandlers( 487 | ghttp.CombineHandlers( 488 | ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/ref/tags/some-tag"), 489 | ghttp.RespondWith(200, `{ "ref": "refs/tags/some-tag", "object" : { "type": "commit", "sha": "some-sha"} }`), 490 | ), 491 | ) 492 | }) 493 | 494 | It("Returns the associated commit SHA", func() { 495 | reference, err := client.ResolveTagToCommitSHA("some-tag") 496 | 497 | Ω(err).ShouldNot(HaveOccurred()) 498 | Expect(reference).To(Equal("some-sha")) 499 | }) 500 | }) 501 | 502 | Context("When GitHub returns a reference to an annotated tag", func() { 503 | BeforeEach(func() { 504 | server.AppendHandlers( 505 | ghttp.CombineHandlers( 506 | ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/ref/tags/some-tag"), 507 | ghttp.RespondWith(200, `{ "ref": "refs/tags/some-tag", "object" : { "type": "tag", "sha": "some-tag-sha"} }`), 508 | ), 509 | ) 510 | }) 511 | 512 | Context("When GitHub returns the annotated tag", func() { 513 | BeforeEach(func() { 514 | server.AppendHandlers( 515 | ghttp.CombineHandlers( 516 | ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/tags/some-tag-sha"), 517 | ghttp.RespondWith(200, `{ "object" : { "type": "commit", "sha": "some-sha"} }`), 518 | ), 519 | ) 520 | }) 521 | 522 | It("Returns the associated commit SHA", func() { 523 | reference, err := client.ResolveTagToCommitSHA("some-tag") 524 | 525 | Ω(err).ShouldNot(HaveOccurred()) 526 | Expect(reference).To(Equal("some-sha")) 527 | }) 528 | }) 529 | 530 | Context("When GitHub fails to fetch the annotated tag", func() { 531 | BeforeEach(func() { 532 | server.AppendHandlers( 533 | ghttp.CombineHandlers( 534 | ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/tags/some-tag-sha"), 535 | ghttp.RespondWith(404, nil), 536 | ), 537 | ) 538 | }) 539 | 540 | It("Returns an error", func() { 541 | _, err := client.ResolveTagToCommitSHA("some-tag") 542 | Ω(err).Should(HaveOccurred()) 543 | }) 544 | }) 545 | 546 | }) 547 | }) 548 | 549 | Describe("DownloadReleaseAsset", func() { 550 | const ( 551 | owner = "bob" 552 | repo = "burgers" 553 | ) 554 | 555 | var ( 556 | assetID int64 557 | asset github.ReleaseAsset 558 | assetPath string 559 | ) 560 | 561 | BeforeEach(func() { 562 | source.Owner = owner 563 | source.Repository = repo 564 | source.AccessToken = "abc123" 565 | assetID = 42 566 | asset = github.ReleaseAsset{ID: &assetID} 567 | assetPath = fmt.Sprintf("/repos/%s/%s/releases/assets/%d", owner, repo, assetID) 568 | }) 569 | 570 | var appendGetHandler = func(server *ghttp.Server, path string, statusCode int, body string, usesAuth bool, headers ...http.Header) { 571 | var authHeaderValue []string 572 | if usesAuth { 573 | authHeaderValue = []string{"Bearer abc123"} 574 | } 575 | server.AppendHandlers(ghttp.CombineHandlers( 576 | ghttp.VerifyRequest("GET", path), 577 | ghttp.RespondWith(statusCode, body, headers...), 578 | ghttp.VerifyHeaderKV("Accept", "application/octet-stream"), 579 | ghttp.VerifyHeaderKV("Authorization", authHeaderValue...), 580 | )) 581 | } 582 | 583 | var locationHeader = func(url string) http.Header { 584 | header := make(http.Header) 585 | header.Add("Location", url) 586 | return header 587 | } 588 | 589 | Context("when the asset can be downloaded directly", func() { 590 | Context("when the asset is downloaded successfully", func() { 591 | const ( 592 | fileContents = "some-random-contents-from-github" 593 | ) 594 | 595 | BeforeEach(func() { 596 | appendGetHandler(server, assetPath, 200, fileContents, true) 597 | }) 598 | 599 | It("returns the correct body", func() { 600 | readCloser, err := client.DownloadReleaseAsset(asset) 601 | Expect(err).NotTo(HaveOccurred()) 602 | defer readCloser.Close() 603 | 604 | body, err := io.ReadAll(readCloser) 605 | Expect(err).NotTo(HaveOccurred()) 606 | Expect(string(body)).To(Equal(fileContents)) 607 | }) 608 | }) 609 | 610 | Context("when there is an error downloading the asset", func() { 611 | BeforeEach(func() { 612 | appendGetHandler(server, assetPath, 401, "authorized personnel only", true) 613 | }) 614 | 615 | It("returns an error", func() { 616 | _, err := client.DownloadReleaseAsset(asset) 617 | Expect(err).To(HaveOccurred()) 618 | }) 619 | }) 620 | }) 621 | 622 | Context("when the asset is behind a redirect", func() { 623 | const redirectPath = "/the/redirect/path" 624 | 625 | BeforeEach(func() { 626 | appendGetHandler(server, assetPath, 307, "", true, locationHeader(redirectPath)) 627 | }) 628 | 629 | Context("when the redirect succeeds", func() { 630 | const ( 631 | redirectFileContents = "some-random-contents-from-redirect" 632 | ) 633 | 634 | BeforeEach(func() { 635 | appendGetHandler(server, redirectPath, 200, redirectFileContents, true) 636 | }) 637 | 638 | It("returns the body from the redirect request", func() { 639 | readCloser, err := client.DownloadReleaseAsset(asset) 640 | Expect(err).NotTo(HaveOccurred()) 641 | defer readCloser.Close() 642 | 643 | body, err := io.ReadAll(readCloser) 644 | Expect(err).NotTo(HaveOccurred()) 645 | Expect(string(body)).To(Equal(redirectFileContents)) 646 | }) 647 | 648 | }) 649 | 650 | Context("when there is another redirect to a URL that succeeds", func() { 651 | const ( 652 | redirectFileContents = "some-random-contents-from-redirect" 653 | ) 654 | 655 | BeforeEach(func() { 656 | appendGetHandler(server, redirectPath, 307, "", true, locationHeader("/somewhere-else")) 657 | appendGetHandler(server, "/somewhere-else", 200, redirectFileContents, true) 658 | }) 659 | 660 | It("returns the body from the final redirect request", func() { 661 | readCloser, err := client.DownloadReleaseAsset(asset) 662 | Expect(err).NotTo(HaveOccurred()) 663 | defer readCloser.Close() 664 | 665 | body, err := io.ReadAll(readCloser) 666 | Expect(err).NotTo(HaveOccurred()) 667 | Expect(string(body)).To(Equal(redirectFileContents)) 668 | }) 669 | }) 670 | 671 | Context("when there is another redirect to an external server", func() { 672 | const ( 673 | redirectFileContents = "some-random-contents-from-redirect" 674 | ) 675 | 676 | var externalServer *ghttp.Server 677 | 678 | BeforeEach(func() { 679 | externalServer = ghttp.NewServer() 680 | u, err := url.Parse(externalServer.URL()) 681 | Expect(err).NotTo(HaveOccurred()) 682 | externalUrl := fmt.Sprintf("http://localhost:%s", u.Port()) 683 | 684 | appendGetHandler(server, redirectPath, 307, "", true, locationHeader(externalUrl+"/somewhere-else")) 685 | appendGetHandler(externalServer, "/somewhere-else", 200, redirectFileContents, false) 686 | }) 687 | 688 | It("downloads the file without the Authorization header", func() { 689 | readCloser, err := client.DownloadReleaseAsset(asset) 690 | Expect(err).NotTo(HaveOccurred()) 691 | defer readCloser.Close() 692 | 693 | body, err := io.ReadAll(readCloser) 694 | Expect(err).NotTo(HaveOccurred()) 695 | Expect(string(body)).To(Equal(redirectFileContents)) 696 | }) 697 | }) 698 | 699 | Context("when the redirect request response is a 400", func() { 700 | BeforeEach(func() { 701 | appendGetHandler(server, redirectPath, 400, "oops", true) 702 | }) 703 | 704 | It("returns an error", func() { 705 | _, err := client.DownloadReleaseAsset(asset) 706 | Expect(err).To(HaveOccurred()) 707 | }) 708 | }) 709 | 710 | Context("when the redirect request response is a 401", func() { 711 | BeforeEach(func() { 712 | appendGetHandler(server, redirectPath, 401, "authorized personnel only", true) 713 | }) 714 | 715 | It("returns an error", func() { 716 | _, err := client.DownloadReleaseAsset(asset) 717 | Expect(err).To(HaveOccurred()) 718 | }) 719 | }) 720 | 721 | Context("when the redirect request response is a 403", func() { 722 | 723 | BeforeEach(func() { 724 | appendGetHandler(server, redirectPath, 403, "authorized personnel only", true) 725 | }) 726 | 727 | It("returns an error", func() { 728 | _, err := client.DownloadReleaseAsset(asset) 729 | Expect(err).To(HaveOccurred()) 730 | }) 731 | }) 732 | 733 | Context("when the redirect request response is a 404", func() { 734 | BeforeEach(func() { 735 | appendGetHandler(server, redirectPath, 404, "I don't know her", true) 736 | }) 737 | 738 | It("returns an error", func() { 739 | _, err := client.DownloadReleaseAsset(asset) 740 | Expect(err).To(HaveOccurred()) 741 | }) 742 | }) 743 | 744 | Context("when the redirect request response is a 500", func() { 745 | BeforeEach(func() { 746 | appendGetHandler(server, redirectPath, 500, "boom", true) 747 | }) 748 | 749 | It("returns an error", func() { 750 | _, err := client.DownloadReleaseAsset(asset) 751 | Expect(err).To(HaveOccurred()) 752 | }) 753 | }) 754 | }) 755 | 756 | Context("when the asset is behind a redirect on an external server", func() { 757 | const ( 758 | redirectFileContents = "some-random-contents-from-redirect" 759 | ) 760 | 761 | var externalServer *ghttp.Server 762 | 763 | BeforeEach(func() { 764 | externalServer = ghttp.NewServer() 765 | 766 | appendGetHandler(server, assetPath, 307, "", true, locationHeader(externalServer.URL()+"/somewhere-else")) 767 | appendGetHandler(externalServer, "/somewhere-else", 200, redirectFileContents, false) 768 | }) 769 | 770 | It("downloads the file without the Authorization header", func() { 771 | readCloser, err := client.DownloadReleaseAsset(asset) 772 | Expect(err).NotTo(HaveOccurred()) 773 | defer readCloser.Close() 774 | 775 | body, err := io.ReadAll(readCloser) 776 | Expect(err).NotTo(HaveOccurred()) 777 | Expect(string(body)).To(Equal(redirectFileContents)) 778 | }) 779 | }) 780 | }) 781 | }) 782 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/concourse/github-release-resource 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/Masterminds/semver v1.5.0 9 | github.com/cppforlife/go-semi-semantic v0.0.0-20160921010311-576b6af77ae4 10 | github.com/google/go-github/v66 v66.0.0 11 | github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 12 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db 13 | github.com/onsi/ginkgo/v2 v2.23.0 14 | github.com/onsi/gomega v1.36.2 15 | github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 16 | golang.org/x/oauth2 v0.30.0 17 | ) 18 | 19 | require ( 20 | github.com/go-logr/logr v1.4.2 // indirect 21 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 22 | github.com/google/go-cmp v0.6.0 // indirect 23 | github.com/google/go-querystring v1.1.0 // indirect 24 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect 25 | github.com/kr/pretty v0.2.1 // indirect 26 | github.com/nxadm/tail v1.4.5 // indirect 27 | github.com/onsi/ginkgo v1.14.2 // indirect 28 | github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect 29 | golang.org/x/mod v0.24.0 // indirect 30 | golang.org/x/net v0.40.0 // indirect 31 | golang.org/x/sync v0.14.0 // indirect 32 | golang.org/x/sys v0.33.0 // indirect 33 | golang.org/x/text v0.25.0 // indirect 34 | golang.org/x/tools v0.33.0 // indirect 35 | google.golang.org/protobuf v1.36.6 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 2 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 3 | github.com/cppforlife/go-semi-semantic v0.0.0-20160921010311-576b6af77ae4 h1:J+ghqo7ZubTzelkjo9hntpTtP/9lUCWH9icEmAW+B+Q= 4 | github.com/cppforlife/go-semi-semantic v0.0.0-20160921010311-576b6af77ae4/go.mod h1:socxpf5+mELPbosI149vWpNlHK6mbfWFxSWOoSndXR8= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 9 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 10 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 11 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 12 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 13 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 14 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 16 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 17 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 18 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 19 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 20 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 21 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 23 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 26 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= 28 | github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= 29 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 30 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 31 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 32 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 33 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 34 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 35 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 36 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 37 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 38 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 39 | github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU= 40 | github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ= 41 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 42 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 43 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 44 | github.com/nxadm/tail v1.4.5 h1:obHEce3upls1IBn1gTw/o7bCv7OJb6Ib/o7wNO+4eKw= 45 | github.com/nxadm/tail v1.4.5/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 46 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 47 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 48 | github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= 49 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 50 | github.com/onsi/ginkgo/v2 v2.23.0 h1:FA1xjp8ieYDzlgS5ABTpdUDB7wtngggONc8a7ku2NqQ= 51 | github.com/onsi/ginkgo/v2 v2.23.0/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= 52 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 53 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 54 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 55 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= 59 | github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= 60 | github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M= 61 | github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= 62 | github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= 63 | github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= 64 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 65 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 66 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 67 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 68 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 69 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 70 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 71 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 72 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 73 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 74 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 75 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 76 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 77 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 78 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 79 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 80 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 86 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 87 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 88 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 89 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 90 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 91 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 92 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 93 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 94 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 95 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 96 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 97 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 98 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 99 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 100 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 101 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 102 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 103 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 105 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 106 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 107 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 108 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 109 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 110 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 111 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 112 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 113 | -------------------------------------------------------------------------------- /in_command.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | 12 | "github.com/google/go-github/v66/github" 13 | ) 14 | 15 | type InCommand struct { 16 | github GitHub 17 | writer io.Writer 18 | } 19 | 20 | func NewInCommand(github GitHub, writer io.Writer) *InCommand { 21 | return &InCommand{ 22 | github: github, 23 | writer: writer, 24 | } 25 | } 26 | 27 | func (c *InCommand) Run(destDir string, request InRequest) (InResponse, error) { 28 | err := os.MkdirAll(destDir, 0755) 29 | if err != nil { 30 | return InResponse{}, err 31 | } 32 | 33 | // if AssetDir is true, create a separate directory for assets 34 | assetDir := destDir 35 | if request.Source.AssetDir { 36 | assetDir = filepath.Join(destDir, "assets") 37 | err = os.MkdirAll(assetDir, 0755) 38 | if err != nil { 39 | return InResponse{}, err 40 | } 41 | } 42 | 43 | var foundRelease *github.RepositoryRelease 44 | var commitSHA string 45 | 46 | id, _ := strconv.Atoi(request.Version.ID) 47 | foundRelease, err = c.github.GetRelease(id) 48 | if err != nil { 49 | foundRelease, err = c.github.GetReleaseByTag(request.Version.Tag) 50 | if err != nil { 51 | return InResponse{}, err 52 | } 53 | } 54 | 55 | if foundRelease == nil { 56 | return InResponse{}, errors.New("no releases") 57 | } 58 | 59 | if foundRelease.HTMLURL != nil && *foundRelease.HTMLURL != "" { 60 | urlPath := filepath.Join(destDir, "url") 61 | err = os.WriteFile(urlPath, []byte(*foundRelease.HTMLURL), 0644) 62 | if err != nil { 63 | return InResponse{}, err 64 | } 65 | } 66 | 67 | if foundRelease.TagName != nil && *foundRelease.TagName != "" { 68 | tagPath := filepath.Join(destDir, "tag") 69 | err = os.WriteFile(tagPath, []byte(*foundRelease.TagName), 0644) 70 | if err != nil { 71 | return InResponse{}, err 72 | } 73 | 74 | versionParser, err := newVersionParser(request.Source.TagFilter) 75 | if err != nil { 76 | return InResponse{}, err 77 | } 78 | version := versionParser.parse(*foundRelease.TagName) 79 | versionPath := filepath.Join(destDir, "version") 80 | err = os.WriteFile(versionPath, []byte(version), 0644) 81 | if err != nil { 82 | return InResponse{}, err 83 | } 84 | 85 | if foundRelease.Draft != nil && !*foundRelease.Draft { 86 | commitPath := filepath.Join(destDir, "commit_sha") 87 | commitSHA, err = c.github.ResolveTagToCommitSHA(*foundRelease.TagName) 88 | if err != nil { 89 | return InResponse{}, err 90 | } 91 | 92 | if commitSHA != "" { 93 | err = os.WriteFile(commitPath, []byte(commitSHA), 0644) 94 | if err != nil { 95 | return InResponse{}, err 96 | } 97 | } 98 | } 99 | 100 | if foundRelease.Body != nil && *foundRelease.Body != "" { 101 | body := *foundRelease.Body 102 | bodyPath := filepath.Join(destDir, "body") 103 | err = os.WriteFile(bodyPath, []byte(body), 0644) 104 | if err != nil { 105 | return InResponse{}, err 106 | } 107 | } 108 | 109 | if foundRelease.PublishedAt != nil || foundRelease.CreatedAt != nil { 110 | timestampPath := filepath.Join(destDir, "timestamp") 111 | timestamp, err := getTimestamp(foundRelease).MarshalText() 112 | if err != nil { 113 | return InResponse{}, err 114 | } 115 | err = os.WriteFile(timestampPath, timestamp, 0644) 116 | if err != nil { 117 | return InResponse{}, err 118 | } 119 | } 120 | } 121 | 122 | assets, err := c.github.ListReleaseAssets(*foundRelease) 123 | if err != nil { 124 | return InResponse{}, err 125 | } 126 | 127 | for _, asset := range assets { 128 | state := asset.State 129 | if state == nil || *state != "uploaded" { 130 | continue 131 | } 132 | 133 | path := filepath.Join(assetDir, *asset.Name) 134 | 135 | var matchFound bool 136 | if len(request.Params.Globs) == 0 { 137 | matchFound = true 138 | } else { 139 | for _, glob := range request.Params.Globs { 140 | matches, err := filepath.Match(glob, *asset.Name) 141 | if err != nil { 142 | return InResponse{}, err 143 | } 144 | 145 | if matches { 146 | matchFound = true 147 | break 148 | } 149 | } 150 | } 151 | 152 | if !matchFound { 153 | continue 154 | } 155 | 156 | fmt.Fprintf(c.writer, "downloading asset: %s\n", *asset.Name) 157 | 158 | err := c.downloadAsset(asset, path) 159 | if err != nil { 160 | return InResponse{}, err 161 | } 162 | } 163 | 164 | if request.Params.IncludeSourceTarball && foundRelease.TagName != nil { 165 | u, err := c.github.GetTarballLink(*foundRelease.TagName) 166 | if err != nil { 167 | return InResponse{}, err 168 | } 169 | fmt.Fprintln(c.writer, "downloading source tarball to source.tar.gz") 170 | if err := c.downloadFile(u.String(), filepath.Join(assetDir, "source.tar.gz")); err != nil { 171 | return InResponse{}, err 172 | } 173 | } 174 | 175 | if request.Params.IncludeSourceZip && foundRelease.TagName != nil { 176 | u, err := c.github.GetZipballLink(*foundRelease.TagName) 177 | if err != nil { 178 | return InResponse{}, err 179 | } 180 | fmt.Fprintln(c.writer, "downloading source zip to source.zip") 181 | if err := c.downloadFile(u.String(), filepath.Join(assetDir, "source.zip")); err != nil { 182 | return InResponse{}, err 183 | } 184 | } 185 | 186 | return InResponse{ 187 | Version: versionFromRelease(foundRelease), 188 | Metadata: metadataFromRelease(foundRelease, commitSHA), 189 | }, nil 190 | } 191 | 192 | func (c *InCommand) downloadAsset(asset *github.ReleaseAsset, destPath string) error { 193 | out, err := os.Create(destPath) 194 | if err != nil { 195 | return err 196 | } 197 | defer out.Close() 198 | 199 | content, err := c.github.DownloadReleaseAsset(*asset) 200 | if err != nil { 201 | return err 202 | } 203 | defer content.Close() 204 | 205 | _, err = io.Copy(out, content) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | return nil 211 | } 212 | 213 | func (c *InCommand) downloadFile(url, destPath string) error { 214 | out, err := os.Create(destPath) 215 | if err != nil { 216 | return err 217 | } 218 | defer out.Close() 219 | 220 | resp, err := http.Get(url) 221 | if err != nil { 222 | return err 223 | } 224 | defer resp.Body.Close() 225 | 226 | if resp.StatusCode != http.StatusOK { 227 | return fmt.Errorf("failed to download file `%s`: HTTP status %d", filepath.Base(destPath), resp.StatusCode) 228 | } 229 | 230 | _, err = io.Copy(out, resp.Body) 231 | if err != nil { 232 | return err 233 | } 234 | 235 | return nil 236 | } 237 | -------------------------------------------------------------------------------- /in_command_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | "github.com/onsi/gomega/ghttp" 16 | 17 | "github.com/google/go-github/v66/github" 18 | 19 | resource "github.com/concourse/github-release-resource" 20 | "github.com/concourse/github-release-resource/fakes" 21 | ) 22 | 23 | var _ = Describe("In Command", func() { 24 | var ( 25 | command *resource.InCommand 26 | githubClient *fakes.FakeGitHub 27 | githubServer *ghttp.Server 28 | 29 | inRequest resource.InRequest 30 | 31 | inResponse resource.InResponse 32 | inErr error 33 | 34 | tmpDir string 35 | destDir string 36 | ) 37 | 38 | BeforeEach(func() { 39 | var err error 40 | 41 | githubClient = &fakes.FakeGitHub{} 42 | githubServer = ghttp.NewServer() 43 | command = resource.NewInCommand(githubClient, io.Discard) 44 | 45 | tmpDir, err = os.MkdirTemp("", "github-release") 46 | Ω(err).ShouldNot(HaveOccurred()) 47 | 48 | destDir = filepath.Join(tmpDir, "destination") 49 | 50 | githubClient.DownloadReleaseAssetReturns(io.NopCloser(bytes.NewBufferString("some-content")), nil) 51 | 52 | inRequest = resource.InRequest{} 53 | }) 54 | 55 | AfterEach(func() { 56 | Ω(os.RemoveAll(tmpDir)).Should(Succeed()) 57 | }) 58 | 59 | buildRelease := func(id int64, tag string, draft bool) *github.RepositoryRelease { 60 | return &github.RepositoryRelease{ 61 | ID: github.Int64(id), 62 | TagName: github.String(tag), 63 | HTMLURL: github.String("http://google.com"), 64 | Name: github.String("release-name"), 65 | Body: github.String("*markdown*"), 66 | CreatedAt: &github.Timestamp{Time: exampleTimeStamp(1)}, 67 | PublishedAt: &github.Timestamp{Time: exampleTimeStamp(1)}, 68 | Draft: github.Bool(draft), 69 | Prerelease: github.Bool(false), 70 | } 71 | } 72 | 73 | buildNilTagRelease := func(id int64) *github.RepositoryRelease { 74 | return &github.RepositoryRelease{ 75 | ID: github.Int64(id), 76 | HTMLURL: github.String("http://google.com"), 77 | Name: github.String("release-name"), 78 | Body: github.String("*markdown*"), 79 | CreatedAt: &github.Timestamp{Time: exampleTimeStamp(1)}, 80 | Draft: github.Bool(true), 81 | Prerelease: github.Bool(false), 82 | } 83 | } 84 | 85 | buildAsset := func(id int64, name string) *github.ReleaseAsset { 86 | state := "uploaded" 87 | return &github.ReleaseAsset{ 88 | ID: github.Int64(id), 89 | Name: &name, 90 | State: &state, 91 | } 92 | } 93 | 94 | buildFailedAsset := func(id int64, name string) *github.ReleaseAsset { 95 | state := "starter" 96 | return &github.ReleaseAsset{ 97 | ID: github.Int64(id), 98 | Name: &name, 99 | State: &state, 100 | } 101 | } 102 | 103 | Context("when there is a tagged release", func() { 104 | Context("when a present version is specified", func() { 105 | BeforeEach(func() { 106 | githubClient.GetReleaseReturns(buildRelease(1, "v0.35.0", false), nil) 107 | githubClient.ResolveTagToCommitSHAReturns("f28085a4a8f744da83411f5e09fd7b1709149eee", nil) 108 | 109 | githubClient.ListReleaseAssetsReturns([]*github.ReleaseAsset{ 110 | buildAsset(0, "example.txt"), 111 | buildAsset(1, "example.rtf"), 112 | buildAsset(2, "example.wtf"), 113 | buildFailedAsset(3, "example.doc"), 114 | }, nil) 115 | 116 | inRequest.Version = &resource.Version{ 117 | ID: "1", 118 | Tag: "v0.35.0", 119 | } 120 | }) 121 | 122 | Context("when valid asset filename globs are given", func() { 123 | BeforeEach(func() { 124 | inRequest.Params = resource.InParams{ 125 | Globs: []string{"*.txt", "*.rtf"}, 126 | } 127 | }) 128 | 129 | It("succeeds", func() { 130 | inResponse, inErr = command.Run(destDir, inRequest) 131 | 132 | Ω(inErr).ShouldNot(HaveOccurred()) 133 | }) 134 | 135 | It("returns the fetched version", func() { 136 | inResponse, inErr = command.Run(destDir, inRequest) 137 | 138 | Ω(inResponse.Version).Should(Equal(newVersionWithTimestamp(1, "v0.35.0", 1))) 139 | }) 140 | 141 | It("has some sweet metadata", func() { 142 | inResponse, inErr = command.Run(destDir, inRequest) 143 | 144 | Ω(inResponse.Metadata).Should(ConsistOf( 145 | resource.MetadataPair{Name: "url", Value: "http://google.com"}, 146 | resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"}, 147 | resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true}, 148 | resource.MetadataPair{Name: "tag", Value: "v0.35.0"}, 149 | resource.MetadataPair{Name: "commit_sha", Value: "f28085a4a8f744da83411f5e09fd7b1709149eee"}, 150 | )) 151 | }) 152 | 153 | It("calls #GetReleast with the correct arguments", func() { 154 | command.Run(destDir, inRequest) 155 | 156 | Ω(githubClient.GetReleaseArgsForCall(0)).Should(Equal(1)) 157 | }) 158 | 159 | It("downloads only the files that match the globs", func() { 160 | inResponse, inErr = command.Run(destDir, inRequest) 161 | 162 | Expect(githubClient.DownloadReleaseAssetCallCount()).To(Equal(2)) 163 | Ω(githubClient.DownloadReleaseAssetArgsForCall(0)).Should(Equal(*buildAsset(0, "example.txt"))) 164 | Ω(githubClient.DownloadReleaseAssetArgsForCall(1)).Should(Equal(*buildAsset(1, "example.rtf"))) 165 | }) 166 | 167 | It("does create the body, tag, version, and url files", func() { 168 | inResponse, inErr = command.Run(destDir, inRequest) 169 | 170 | contents, err := os.ReadFile(path.Join(destDir, "tag")) 171 | Ω(err).ShouldNot(HaveOccurred()) 172 | Ω(string(contents)).Should(Equal("v0.35.0")) 173 | 174 | contents, err = os.ReadFile(path.Join(destDir, "version")) 175 | Ω(err).ShouldNot(HaveOccurred()) 176 | Ω(string(contents)).Should(Equal("0.35.0")) 177 | 178 | contents, err = os.ReadFile(path.Join(destDir, "commit_sha")) 179 | Ω(err).ShouldNot(HaveOccurred()) 180 | Ω(string(contents)).Should(Equal("f28085a4a8f744da83411f5e09fd7b1709149eee")) 181 | 182 | contents, err = os.ReadFile(path.Join(destDir, "body")) 183 | Ω(err).ShouldNot(HaveOccurred()) 184 | Ω(string(contents)).Should(Equal("*markdown*")) 185 | 186 | contents, err = os.ReadFile(path.Join(destDir, "timestamp")) 187 | Ω(err).ShouldNot(HaveOccurred()) 188 | Ω(string(contents)).Should(Equal("2018-01-01T00:00:00Z")) 189 | 190 | contents, err = os.ReadFile(path.Join(destDir, "url")) 191 | Ω(err).ShouldNot(HaveOccurred()) 192 | Ω(string(contents)).Should(Equal("http://google.com")) 193 | }) 194 | 195 | Context("when there is a custom tag filter", func() { 196 | BeforeEach(func() { 197 | inRequest.Source = resource.Source{ 198 | TagFilter: "package-(.*)", 199 | } 200 | githubClient.GetReleaseReturns(buildRelease(1, "package-0.35.0", false), nil) 201 | githubClient.ResolveTagToCommitSHAReturns("f28085a4a8f744da83411f5e09fd7b1709149eee", nil) 202 | inResponse, inErr = command.Run(destDir, inRequest) 203 | }) 204 | 205 | It("succeeds", func() { 206 | inResponse, inErr = command.Run(destDir, inRequest) 207 | 208 | Expect(inErr).ToNot(HaveOccurred()) 209 | }) 210 | 211 | It("does create the tag, version, and url files", func() { 212 | inResponse, inErr = command.Run(destDir, inRequest) 213 | 214 | contents, err := os.ReadFile(path.Join(destDir, "tag")) 215 | Ω(err).ShouldNot(HaveOccurred()) 216 | Ω(string(contents)).Should(Equal("package-0.35.0")) 217 | 218 | contents, err = os.ReadFile(path.Join(destDir, "version")) 219 | Ω(err).ShouldNot(HaveOccurred()) 220 | Ω(string(contents)).Should(Equal("0.35.0")) 221 | 222 | contents, err = os.ReadFile(path.Join(destDir, "url")) 223 | Ω(err).ShouldNot(HaveOccurred()) 224 | Ω(string(contents)).Should(Equal("http://google.com")) 225 | }) 226 | }) 227 | 228 | Context("when include_source_tarball is true", func() { 229 | var tarballUrl *url.URL 230 | 231 | BeforeEach(func() { 232 | inRequest.Params.IncludeSourceTarball = true 233 | 234 | tarballUrl, _ = url.Parse(githubServer.URL()) 235 | tarballUrl.Path = "/gimme-a-tarball/" 236 | }) 237 | 238 | Context("when getting the tarball link succeeds", func() { 239 | BeforeEach(func() { 240 | githubClient.GetTarballLinkReturns(tarballUrl, nil) 241 | }) 242 | 243 | Context("when downloading the tarball succeeds", func() { 244 | BeforeEach(func() { 245 | githubServer.AppendHandlers( 246 | ghttp.CombineHandlers( 247 | ghttp.VerifyRequest("GET", tarballUrl.Path), 248 | ghttp.RespondWith(http.StatusOK, "source-tar-file-contents"), 249 | ), 250 | ) 251 | }) 252 | 253 | It("succeeds", func() { 254 | inResponse, inErr = command.Run(destDir, inRequest) 255 | 256 | Expect(inErr).ToNot(HaveOccurred()) 257 | }) 258 | 259 | It("downloads the source tarball", func() { 260 | inResponse, inErr = command.Run(destDir, inRequest) 261 | 262 | Expect(githubServer.ReceivedRequests()).To(HaveLen(1)) 263 | }) 264 | 265 | It("saves the source tarball in the destination directory", func() { 266 | inResponse, inErr = command.Run(destDir, inRequest) 267 | 268 | fileContents, err := os.ReadFile(filepath.Join(destDir, "source.tar.gz")) 269 | fContents := string(fileContents) 270 | Expect(err).NotTo(HaveOccurred()) 271 | Expect(fContents).To(Equal("source-tar-file-contents")) 272 | }) 273 | 274 | It("saves the source tarball in the assets directory, if desired", func() { 275 | inRequest.Source.AssetDir = true 276 | inResponse, inErr = command.Run(destDir, inRequest) 277 | 278 | fileContents, err := os.ReadFile(filepath.Join(destDir, "assets", "source.tar.gz")) 279 | fContents := string(fileContents) 280 | Expect(err).NotTo(HaveOccurred()) 281 | Expect(fContents).To(Equal("source-tar-file-contents")) 282 | }) 283 | }) 284 | 285 | Context("when downloading the tarball fails", func() { 286 | BeforeEach(func() { 287 | githubServer.AppendHandlers( 288 | ghttp.CombineHandlers( 289 | ghttp.VerifyRequest("GET", tarballUrl.Path), 290 | ghttp.RespondWith(http.StatusInternalServerError, ""), 291 | ), 292 | ) 293 | }) 294 | 295 | It("returns an appropriate error", func() { 296 | inResponse, inErr = command.Run(destDir, inRequest) 297 | 298 | Expect(inErr).To(MatchError("failed to download file `source.tar.gz`: HTTP status 500")) 299 | }) 300 | }) 301 | }) 302 | 303 | Context("when getting the tarball link fails", func() { 304 | disaster := errors.New("oh my") 305 | 306 | BeforeEach(func() { 307 | githubClient.GetTarballLinkReturns(nil, disaster) 308 | }) 309 | 310 | It("returns the error", func() { 311 | inResponse, inErr = command.Run(destDir, inRequest) 312 | 313 | Expect(inErr).To(Equal(disaster)) 314 | }) 315 | }) 316 | }) 317 | 318 | Context("when include_source_zip is true", func() { 319 | var zipUrl *url.URL 320 | 321 | BeforeEach(func() { 322 | inRequest.Params.IncludeSourceZip = true 323 | 324 | zipUrl, _ = url.Parse(githubServer.URL()) 325 | zipUrl.Path = "/gimme-a-zip/" 326 | }) 327 | 328 | Context("when getting the zip link succeeds", func() { 329 | BeforeEach(func() { 330 | githubClient.GetZipballLinkReturns(zipUrl, nil) 331 | }) 332 | 333 | Context("when downloading the zip succeeds", func() { 334 | BeforeEach(func() { 335 | githubServer.AppendHandlers( 336 | ghttp.CombineHandlers( 337 | ghttp.VerifyRequest("GET", zipUrl.Path), 338 | ghttp.RespondWith(http.StatusOK, "source-zip-file-contents"), 339 | ), 340 | ) 341 | }) 342 | 343 | It("succeeds", func() { 344 | inResponse, inErr = command.Run(destDir, inRequest) 345 | 346 | Expect(inErr).ToNot(HaveOccurred()) 347 | }) 348 | 349 | It("downloads the source zip", func() { 350 | inResponse, inErr = command.Run(destDir, inRequest) 351 | 352 | Expect(githubServer.ReceivedRequests()).To(HaveLen(1)) 353 | }) 354 | 355 | It("saves the source zip in the destination directory", func() { 356 | inResponse, inErr = command.Run(destDir, inRequest) 357 | 358 | fileContents, err := os.ReadFile(filepath.Join(destDir, "source.zip")) 359 | fContents := string(fileContents) 360 | Expect(err).NotTo(HaveOccurred()) 361 | Expect(fContents).To(Equal("source-zip-file-contents")) 362 | }) 363 | 364 | It("saves the source tarball in the assets directory, if desired", func() { 365 | inRequest.Source.AssetDir = true 366 | inResponse, inErr = command.Run(destDir, inRequest) 367 | 368 | fileContents, err := os.ReadFile(filepath.Join(destDir, "assets", "source.zip")) 369 | fContents := string(fileContents) 370 | Expect(err).NotTo(HaveOccurred()) 371 | Expect(fContents).To(Equal("source-zip-file-contents")) 372 | }) 373 | }) 374 | 375 | Context("when downloading the zip fails", func() { 376 | BeforeEach(func() { 377 | githubServer.AppendHandlers( 378 | ghttp.CombineHandlers( 379 | ghttp.VerifyRequest("GET", zipUrl.Path), 380 | ghttp.RespondWith(http.StatusInternalServerError, ""), 381 | ), 382 | ) 383 | }) 384 | 385 | It("returns an appropriate error", func() { 386 | inResponse, inErr = command.Run(destDir, inRequest) 387 | 388 | Expect(inErr).To(MatchError("failed to download file `source.zip`: HTTP status 500")) 389 | }) 390 | }) 391 | }) 392 | 393 | Context("when getting the zip link fails", func() { 394 | disaster := errors.New("oh my") 395 | 396 | BeforeEach(func() { 397 | githubClient.GetZipballLinkReturns(nil, disaster) 398 | }) 399 | 400 | It("returns the error", func() { 401 | inResponse, inErr = command.Run(destDir, inRequest) 402 | 403 | Expect(inErr).To(Equal(disaster)) 404 | }) 405 | }) 406 | }) 407 | }) 408 | 409 | Context("when no globs are specified", func() { 410 | BeforeEach(func() { 411 | inRequest.Source = resource.Source{} 412 | inResponse, inErr = command.Run(destDir, inRequest) 413 | }) 414 | 415 | It("succeeds", func() { 416 | Ω(inErr).ShouldNot(HaveOccurred()) 417 | }) 418 | 419 | It("returns the fetched version", func() { 420 | Ω(inResponse.Version).Should(Equal(newVersionWithTimestamp(1, "v0.35.0", 1))) 421 | }) 422 | 423 | It("has some sweet metadata", func() { 424 | Ω(inResponse.Metadata).Should(ConsistOf( 425 | resource.MetadataPair{Name: "url", Value: "http://google.com"}, 426 | resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"}, 427 | resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true}, 428 | resource.MetadataPair{Name: "tag", Value: "v0.35.0"}, 429 | resource.MetadataPair{Name: "commit_sha", Value: "f28085a4a8f744da83411f5e09fd7b1709149eee"}, 430 | )) 431 | }) 432 | 433 | It("downloads all of the files", func() { 434 | Ω(githubClient.DownloadReleaseAssetArgsForCall(0)).Should(Equal(*buildAsset(0, "example.txt"))) 435 | Ω(githubClient.DownloadReleaseAssetArgsForCall(1)).Should(Equal(*buildAsset(1, "example.rtf"))) 436 | Ω(githubClient.DownloadReleaseAssetArgsForCall(2)).Should(Equal(*buildAsset(2, "example.wtf"))) 437 | Ω(githubClient.DownloadReleaseAssetCallCount()).Should(Equal(3)) 438 | }) 439 | }) 440 | 441 | Context("when downloading an asset fails", func() { 442 | BeforeEach(func() { 443 | githubClient.DownloadReleaseAssetReturns(nil, errors.New("not this time")) 444 | inResponse, inErr = command.Run(destDir, inRequest) 445 | }) 446 | 447 | It("returns an error", func() { 448 | Ω(inErr).Should(HaveOccurred()) 449 | }) 450 | }) 451 | 452 | Context("when listing release assets fails", func() { 453 | disaster := errors.New("nope") 454 | 455 | BeforeEach(func() { 456 | githubClient.ListReleaseAssetsReturns(nil, disaster) 457 | inResponse, inErr = command.Run(destDir, inRequest) 458 | }) 459 | 460 | It("returns the error", func() { 461 | Ω(inErr).Should(Equal(disaster)) 462 | }) 463 | }) 464 | }) 465 | }) 466 | 467 | Context("when no tagged release is present", func() { 468 | BeforeEach(func() { 469 | githubClient.GetReleaseReturns(nil, nil) 470 | 471 | inRequest.Version = &resource.Version{ 472 | Tag: "v0.40.0", 473 | } 474 | 475 | inResponse, inErr = command.Run(destDir, inRequest) 476 | }) 477 | 478 | It("returns an error", func() { 479 | Ω(inErr).Should(MatchError("no releases")) 480 | }) 481 | }) 482 | 483 | Context("when getting a tagged release fails", func() { 484 | disaster := errors.New("no releases") 485 | 486 | BeforeEach(func() { 487 | githubClient.GetReleaseReturns(nil, disaster) 488 | 489 | inRequest.Version = &resource.Version{ 490 | Tag: "some-tag", 491 | } 492 | inResponse, inErr = command.Run(destDir, inRequest) 493 | }) 494 | 495 | It("returns the error", func() { 496 | Ω(inErr).Should(Equal(disaster)) 497 | }) 498 | }) 499 | 500 | Context("when there is a draft release", func() { 501 | Context("which has a tag", func() { 502 | BeforeEach(func() { 503 | githubClient.GetReleaseReturns(buildRelease(1, "v0.35.0", true), nil) 504 | 505 | inRequest.Version = &resource.Version{ID: "1"} 506 | inResponse, inErr = command.Run(destDir, inRequest) 507 | }) 508 | 509 | It("succeeds", func() { 510 | Ω(inErr).ShouldNot(HaveOccurred()) 511 | }) 512 | 513 | It("returns the fetched version", func() { 514 | Ω(inResponse.Version).Should(Equal(newVersionWithTimestamp(1, "v0.35.0", 1))) 515 | }) 516 | 517 | It("has some sweet metadata", func() { 518 | Ω(inResponse.Metadata).Should(ConsistOf( 519 | resource.MetadataPair{Name: "url", Value: "http://google.com"}, 520 | resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"}, 521 | resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true}, 522 | resource.MetadataPair{Name: "tag", Value: "v0.35.0"}, 523 | resource.MetadataPair{Name: "draft", Value: "true"}, 524 | )) 525 | }) 526 | 527 | It("does create the tag, version, and URL files", func() { 528 | contents, err := os.ReadFile(path.Join(destDir, "tag")) 529 | Ω(err).ShouldNot(HaveOccurred()) 530 | Ω(string(contents)).Should(Equal("v0.35.0")) 531 | 532 | contents, err = os.ReadFile(path.Join(destDir, "version")) 533 | Ω(err).ShouldNot(HaveOccurred()) 534 | Ω(string(contents)).Should(Equal("0.35.0")) 535 | 536 | contents, err = os.ReadFile(path.Join(destDir, "url")) 537 | Ω(err).ShouldNot(HaveOccurred()) 538 | Ω(string(contents)).Should(Equal("http://google.com")) 539 | }) 540 | }) 541 | 542 | Context("which has an empty tag", func() { 543 | BeforeEach(func() { 544 | githubClient.GetReleaseReturns(buildRelease(1, "", true), nil) 545 | 546 | inRequest.Version = &resource.Version{ID: "1"} 547 | inResponse, inErr = command.Run(destDir, inRequest) 548 | }) 549 | 550 | It("succeeds", func() { 551 | Ω(inErr).ShouldNot(HaveOccurred()) 552 | }) 553 | 554 | It("returns the fetched version", func() { 555 | Ω(inResponse.Version).Should(Equal(newVersionWithTimestamp(1, "", 1))) 556 | }) 557 | 558 | It("has some sweet metadata", func() { 559 | Ω(inResponse.Metadata).Should(ConsistOf( 560 | resource.MetadataPair{Name: "url", Value: "http://google.com"}, 561 | resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"}, 562 | resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true}, 563 | resource.MetadataPair{Name: "tag", Value: ""}, 564 | resource.MetadataPair{Name: "draft", Value: "true"}, 565 | )) 566 | }) 567 | 568 | It("does not create the tag and version files", func() { 569 | Ω(path.Join(destDir, "tag")).ShouldNot(BeAnExistingFile()) 570 | Ω(path.Join(destDir, "version")).ShouldNot(BeAnExistingFile()) 571 | Ω(path.Join(destDir, "commit_sha")).ShouldNot(BeAnExistingFile()) 572 | }) 573 | 574 | It("does create the url file", func() { 575 | contents, err := os.ReadFile(path.Join(destDir, "url")) 576 | Ω(err).ShouldNot(HaveOccurred()) 577 | Ω(string(contents)).Should(Equal("http://google.com")) 578 | }) 579 | }) 580 | 581 | Context("which has a nil tag", func() { 582 | BeforeEach(func() { 583 | githubClient.GetReleaseReturns(buildNilTagRelease(1), nil) 584 | 585 | inRequest.Version = &resource.Version{ID: "1"} 586 | inResponse, inErr = command.Run(destDir, inRequest) 587 | }) 588 | 589 | It("succeeds", func() { 590 | Ω(inErr).ShouldNot(HaveOccurred()) 591 | }) 592 | 593 | It("returns the fetched version", func() { 594 | Ω(inResponse.Version).Should(Equal(newVersionWithTimestamp(1, "", 1))) 595 | }) 596 | 597 | It("has some sweet metadata", func() { 598 | Ω(inResponse.Metadata).Should(ConsistOf( 599 | resource.MetadataPair{Name: "url", Value: "http://google.com"}, 600 | resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"}, 601 | resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true}, 602 | resource.MetadataPair{Name: "draft", Value: "true"}, 603 | )) 604 | }) 605 | 606 | It("does not create the tag and version files", func() { 607 | Ω(path.Join(destDir, "tag")).ShouldNot(BeAnExistingFile()) 608 | Ω(path.Join(destDir, "version")).ShouldNot(BeAnExistingFile()) 609 | Ω(path.Join(destDir, "commit_sha")).ShouldNot(BeAnExistingFile()) 610 | }) 611 | 612 | It("does create the url file", func() { 613 | contents, err := os.ReadFile(path.Join(destDir, "url")) 614 | Ω(err).ShouldNot(HaveOccurred()) 615 | Ω(string(contents)).Should(Equal("http://google.com")) 616 | }) 617 | }) 618 | }) 619 | }) 620 | -------------------------------------------------------------------------------- /metadata.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "github.com/google/go-github/v66/github" 4 | 5 | func metadataFromRelease(release *github.RepositoryRelease, commitSHA string) []MetadataPair { 6 | metadata := []MetadataPair{} 7 | 8 | if release.Name != nil { 9 | nameMeta := MetadataPair{ 10 | Name: "name", 11 | Value: *release.Name, 12 | } 13 | 14 | if release.HTMLURL != nil { 15 | nameMeta.URL = *release.HTMLURL 16 | } 17 | 18 | metadata = append(metadata, nameMeta) 19 | } 20 | 21 | if release.HTMLURL != nil { 22 | metadata = append(metadata, MetadataPair{ 23 | Name: "url", 24 | Value: *release.HTMLURL, 25 | }) 26 | } 27 | 28 | if release.Body != nil { 29 | metadata = append(metadata, MetadataPair{ 30 | Name: "body", 31 | Value: *release.Body, 32 | Markdown: true, 33 | }) 34 | } 35 | 36 | if release.TagName != nil { 37 | metadata = append(metadata, MetadataPair{ 38 | Name: "tag", 39 | Value: *release.TagName, 40 | }) 41 | } 42 | 43 | if commitSHA != "" { 44 | metadata = append(metadata, MetadataPair{ 45 | Name: "commit_sha", 46 | Value: commitSHA, 47 | }) 48 | } 49 | 50 | if *release.Draft { 51 | metadata = append(metadata, MetadataPair{ 52 | Name: "draft", 53 | Value: "true", 54 | }) 55 | } 56 | 57 | if *release.Prerelease { 58 | metadata = append(metadata, MetadataPair{ 59 | Name: "pre-release", 60 | Value: "true", 61 | }) 62 | } 63 | return metadata 64 | } 65 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "github.com/shurcooL/githubv4" 4 | 5 | // ReleaseObject represent the graphql release object 6 | // https://developer.github.com/v4/object/release 7 | type ReleaseObject struct { 8 | CreatedAt githubv4.DateTime `graphql:"createdAt"` 9 | PublishedAt githubv4.DateTime `graphql:"publishedAt"` 10 | ID string `graphql:"id"` 11 | DatabaseId githubv4.Int `graphql:"databaseId"` 12 | IsDraft bool `graphql:"isDraft"` 13 | IsPrerelease bool `graphql:"isPrerelease"` 14 | Name string `graphql:"name"` 15 | TagName string `graphql:"tagName"` 16 | URL string `graphql:"url"` 17 | } 18 | 19 | // ReleaseObjectEnterprise Workaround until DatabaseId will appear in enterprise installation 20 | // https://github.com/concourse/github-release-resource/issues/109 21 | type ReleaseObjectEnterprise struct { 22 | CreatedAt githubv4.DateTime `graphql:"createdAt"` 23 | PublishedAt githubv4.DateTime `graphql:"publishedAt"` 24 | ID string `graphql:"id"` 25 | IsDraft bool `graphql:"isDraft"` 26 | IsPrerelease bool `graphql:"isPrerelease"` 27 | Name string `graphql:"name"` 28 | TagName string `graphql:"tagName"` 29 | URL string `graphql:"url"` 30 | } 31 | -------------------------------------------------------------------------------- /out_command.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/google/go-github/v66/github" 11 | ) 12 | 13 | type OutCommand struct { 14 | github GitHub 15 | writer io.Writer 16 | } 17 | 18 | func NewOutCommand(github GitHub, writer io.Writer) *OutCommand { 19 | return &OutCommand{ 20 | github: github, 21 | writer: writer, 22 | } 23 | } 24 | 25 | func (c *OutCommand) Run(sourceDir string, request OutRequest) (OutResponse, error) { 26 | params := request.Params 27 | 28 | name, err := c.fileContents(filepath.Join(sourceDir, request.Params.NamePath)) 29 | if err != nil { 30 | return OutResponse{}, err 31 | } 32 | 33 | tag, err := c.fileContents(filepath.Join(sourceDir, request.Params.TagPath)) 34 | if err != nil { 35 | return OutResponse{}, err 36 | } 37 | 38 | tag = request.Params.TagPrefix + tag 39 | 40 | var body string 41 | bodySpecified := false 42 | if request.Params.BodyPath != "" { 43 | bodySpecified = true 44 | 45 | body, err = c.fileContents(filepath.Join(sourceDir, request.Params.BodyPath)) 46 | if err != nil { 47 | return OutResponse{}, err 48 | } 49 | } 50 | 51 | targetCommitish := "" 52 | if request.Params.CommitishPath != "" { 53 | targetCommitish, err = c.fileContents(filepath.Join(sourceDir, request.Params.CommitishPath)) 54 | if err != nil { 55 | return OutResponse{}, err 56 | } 57 | } 58 | 59 | draft := request.Source.Drafts 60 | prerelease := false 61 | if request.Source.PreRelease == true && request.Source.Release == false { 62 | prerelease = request.Source.PreRelease 63 | } 64 | 65 | generateReleaseNotes := request.Params.GenerateReleaseNotes 66 | 67 | release := &github.RepositoryRelease{ 68 | Name: github.String(name), 69 | TagName: github.String(tag), 70 | Body: github.String(body), 71 | Draft: github.Bool(draft), 72 | Prerelease: github.Bool(prerelease), 73 | TargetCommitish: github.String(targetCommitish), 74 | GenerateReleaseNotes: github.Bool(generateReleaseNotes), 75 | } 76 | 77 | existingReleases, err := c.github.ListReleases() 78 | if err != nil { 79 | return OutResponse{}, err 80 | } 81 | 82 | var existingRelease *github.RepositoryRelease 83 | for _, e := range existingReleases { 84 | if e.TagName != nil && *e.TagName == tag { 85 | existingRelease = e 86 | break 87 | } 88 | } 89 | 90 | if existingRelease != nil { 91 | releaseAssets, err := c.github.ListReleaseAssets(*existingRelease) 92 | if err != nil { 93 | return OutResponse{}, err 94 | } 95 | 96 | existingRelease.Name = github.String(name) 97 | existingRelease.TargetCommitish = github.String(targetCommitish) 98 | existingRelease.Draft = github.Bool(draft) 99 | existingRelease.Prerelease = github.Bool(prerelease) 100 | 101 | if bodySpecified { 102 | existingRelease.Body = github.String(body) 103 | } else { 104 | existingRelease.Body = nil 105 | } 106 | 107 | for _, asset := range releaseAssets { 108 | fmt.Fprintf(c.writer, "clearing existing asset: %s\n", *asset.Name) 109 | 110 | err := c.github.DeleteReleaseAsset(*asset) 111 | if err != nil { 112 | return OutResponse{}, err 113 | } 114 | } 115 | 116 | fmt.Fprintf(c.writer, "updating release %s\n", name) 117 | 118 | release, err = c.github.UpdateRelease(*existingRelease) 119 | if err != nil { 120 | return OutResponse{}, err 121 | } 122 | } else { 123 | fmt.Fprintf(c.writer, "creating release %s\n", name) 124 | release, err = c.github.CreateRelease(*release) 125 | if err != nil { 126 | return OutResponse{}, err 127 | } 128 | } 129 | 130 | for _, fileGlob := range params.Globs { 131 | matches, err := filepath.Glob(filepath.Join(sourceDir, fileGlob)) 132 | if err != nil { 133 | return OutResponse{}, err 134 | } 135 | 136 | if len(matches) == 0 { 137 | return OutResponse{}, fmt.Errorf("could not find file that matches glob '%s'", fileGlob) 138 | } 139 | 140 | for _, filePath := range matches { 141 | err := c.upload(release, filePath) 142 | if err != nil { 143 | return OutResponse{}, err 144 | } 145 | } 146 | } 147 | 148 | return OutResponse{ 149 | Version: versionFromRelease(release), 150 | Metadata: metadataFromRelease(release, ""), 151 | }, nil 152 | } 153 | 154 | func (c *OutCommand) fileContents(path string) (string, error) { 155 | contents, err := os.ReadFile(path) 156 | if err != nil { 157 | return "", err 158 | } 159 | 160 | return strings.TrimSpace(string(contents)), nil 161 | } 162 | 163 | func (c *OutCommand) upload(release *github.RepositoryRelease, filePath string) error { 164 | fmt.Fprintf(c.writer, "uploading %s\n", filePath) 165 | 166 | name := filepath.Base(filePath) 167 | 168 | var retryErr error 169 | for range 10 { 170 | file, err := os.Open(filePath) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | defer file.Close() 176 | 177 | retryErr = c.github.UploadReleaseAsset(*release, name, file) 178 | if retryErr == nil { 179 | break 180 | } 181 | 182 | assets, err := c.github.ListReleaseAssets(*release) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | for _, asset := range assets { 188 | if asset.Name != nil && *asset.Name == name { 189 | err = c.github.DeleteReleaseAsset(*asset) 190 | if err != nil { 191 | return err 192 | } 193 | break 194 | } 195 | } 196 | } 197 | 198 | if retryErr != nil { 199 | return retryErr 200 | } 201 | 202 | return nil 203 | } 204 | -------------------------------------------------------------------------------- /out_command_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | 12 | resource "github.com/concourse/github-release-resource" 13 | "github.com/concourse/github-release-resource/fakes" 14 | 15 | "github.com/google/go-github/v66/github" 16 | ) 17 | 18 | func file(path, contents string) { 19 | Ω(os.WriteFile(path, []byte(contents), 0644)).Should(Succeed()) 20 | } 21 | 22 | var _ = Describe("Out Command", func() { 23 | var ( 24 | command *resource.OutCommand 25 | githubClient *fakes.FakeGitHub 26 | 27 | sourcesDir string 28 | 29 | request resource.OutRequest 30 | ) 31 | 32 | BeforeEach(func() { 33 | var err error 34 | 35 | githubClient = &fakes.FakeGitHub{} 36 | command = resource.NewOutCommand(githubClient, io.Discard) 37 | 38 | sourcesDir, err = os.MkdirTemp("", "github-release") 39 | Ω(err).ShouldNot(HaveOccurred()) 40 | 41 | githubClient.CreateReleaseStub = func(gh github.RepositoryRelease) (*github.RepositoryRelease, error) { 42 | createdRel := gh 43 | createdRel.ID = github.Int64(112) 44 | createdRel.HTMLURL = github.String("http://google.com") 45 | createdRel.Name = github.String("release-name") 46 | createdRel.Body = github.String("*markdown*") 47 | return &createdRel, nil 48 | } 49 | 50 | githubClient.UpdateReleaseStub = func(gh github.RepositoryRelease) (*github.RepositoryRelease, error) { 51 | return &gh, nil 52 | } 53 | }) 54 | 55 | AfterEach(func() { 56 | Ω(os.RemoveAll(sourcesDir)).Should(Succeed()) 57 | }) 58 | 59 | Context("when the release has already been created", func() { 60 | existingAssets := []github.ReleaseAsset{ 61 | { 62 | ID: github.Int64(456789), 63 | Name: github.String("unicorns.txt"), 64 | }, 65 | { 66 | ID: github.Int64(3450798), 67 | Name: github.String("rainbows.txt"), 68 | State: github.String("new"), 69 | }, 70 | } 71 | 72 | existingReleases := []github.RepositoryRelease{ 73 | { 74 | ID: github.Int64(1), 75 | Draft: github.Bool(true), 76 | }, 77 | { 78 | ID: github.Int64(112), 79 | TagName: github.String("some-tag-name"), 80 | Assets: []*github.ReleaseAsset{&existingAssets[0]}, 81 | Draft: github.Bool(false), 82 | }, 83 | } 84 | 85 | BeforeEach(func() { 86 | githubClient.ListReleasesStub = func() ([]*github.RepositoryRelease, error) { 87 | var rels []*github.RepositoryRelease 88 | for _, r := range existingReleases { 89 | c := r 90 | rels = append(rels, &c) 91 | } 92 | 93 | return rels, nil 94 | } 95 | 96 | githubClient.ListReleaseAssetsStub = func(github.RepositoryRelease) ([]*github.ReleaseAsset, error) { 97 | var assets []*github.ReleaseAsset 98 | for _, a := range existingAssets { 99 | c := a 100 | assets = append(assets, &c) 101 | } 102 | 103 | return assets, nil 104 | } 105 | 106 | namePath := filepath.Join(sourcesDir, "name") 107 | bodyPath := filepath.Join(sourcesDir, "body") 108 | tagPath := filepath.Join(sourcesDir, "tag") 109 | 110 | file(namePath, "v0.3.12") 111 | file(bodyPath, "this is a great release") 112 | file(tagPath, "some-tag-name") 113 | 114 | request = resource.OutRequest{ 115 | Params: resource.OutParams{ 116 | NamePath: "name", 117 | BodyPath: "body", 118 | TagPath: "tag", 119 | }, 120 | } 121 | }) 122 | 123 | It("deletes the existing assets", func() { 124 | _, err := command.Run(sourcesDir, request) 125 | Ω(err).ShouldNot(HaveOccurred()) 126 | 127 | Ω(githubClient.ListReleaseAssetsCallCount()).Should(Equal(1)) 128 | Ω(githubClient.ListReleaseAssetsArgsForCall(0)).Should(Equal(existingReleases[1])) 129 | 130 | Ω(githubClient.DeleteReleaseAssetCallCount()).Should(Equal(2)) 131 | 132 | Ω(githubClient.DeleteReleaseAssetArgsForCall(0)).Should(Equal(existingAssets[0])) 133 | Ω(githubClient.DeleteReleaseAssetArgsForCall(1)).Should(Equal(existingAssets[1])) 134 | }) 135 | 136 | Context("when not set as a draft release", func() { 137 | BeforeEach(func() { 138 | request.Source.Drafts = false 139 | }) 140 | 141 | It("updates the existing release to a non-draft", func() { 142 | _, err := command.Run(sourcesDir, request) 143 | Ω(err).ShouldNot(HaveOccurred()) 144 | 145 | Ω(githubClient.UpdateReleaseCallCount()).Should(Equal(1)) 146 | 147 | updatedRelease := githubClient.UpdateReleaseArgsForCall(0) 148 | Ω(*updatedRelease.Name).Should(Equal("v0.3.12")) 149 | Ω(*updatedRelease.Draft).Should(Equal(false)) 150 | }) 151 | }) 152 | 153 | Context("when set as a draft release", func() { 154 | BeforeEach(func() { 155 | request.Source.Drafts = true 156 | }) 157 | 158 | It("updates the existing release to a draft", func() { 159 | _, err := command.Run(sourcesDir, request) 160 | Ω(err).ShouldNot(HaveOccurred()) 161 | 162 | Ω(githubClient.UpdateReleaseCallCount()).Should(Equal(1)) 163 | 164 | updatedRelease := githubClient.UpdateReleaseArgsForCall(0) 165 | Ω(*updatedRelease.Name).Should(Equal("v0.3.12")) 166 | Ω(*updatedRelease.Draft).Should(Equal(true)) 167 | }) 168 | }) 169 | 170 | Context("when a body is not supplied", func() { 171 | BeforeEach(func() { 172 | request.Params.BodyPath = "" 173 | }) 174 | 175 | It("does not blow away the body", func() { 176 | _, err := command.Run(sourcesDir, request) 177 | Ω(err).ShouldNot(HaveOccurred()) 178 | 179 | Ω(githubClient.UpdateReleaseCallCount()).Should(Equal(1)) 180 | 181 | updatedRelease := githubClient.UpdateReleaseArgsForCall(0) 182 | Ω(*updatedRelease.Name).Should(Equal("v0.3.12")) 183 | Ω(updatedRelease.Body).Should(BeNil()) 184 | }) 185 | }) 186 | 187 | Context("when a commitish is not supplied", func() { 188 | It("updates the existing release", func() { 189 | _, err := command.Run(sourcesDir, request) 190 | Ω(err).ShouldNot(HaveOccurred()) 191 | 192 | Ω(githubClient.UpdateReleaseCallCount()).Should(Equal(1)) 193 | 194 | updatedRelease := githubClient.UpdateReleaseArgsForCall(0) 195 | Ω(*updatedRelease.Name).Should(Equal("v0.3.12")) 196 | Ω(*updatedRelease.Body).Should(Equal("this is a great release")) 197 | Ω(updatedRelease.TargetCommitish).Should(Equal(github.String(""))) 198 | }) 199 | }) 200 | 201 | Context("when a commitish is supplied", func() { 202 | BeforeEach(func() { 203 | commitishPath := filepath.Join(sourcesDir, "commitish") 204 | file(commitishPath, "1z22f1") 205 | request.Params.CommitishPath = "commitish" 206 | }) 207 | 208 | It("updates the existing release", func() { 209 | _, err := command.Run(sourcesDir, request) 210 | Ω(err).ShouldNot(HaveOccurred()) 211 | 212 | Ω(githubClient.UpdateReleaseCallCount()).Should(Equal(1)) 213 | 214 | updatedRelease := githubClient.UpdateReleaseArgsForCall(0) 215 | Ω(*updatedRelease.Name).Should(Equal("v0.3.12")) 216 | Ω(*updatedRelease.Body).Should(Equal("this is a great release")) 217 | Ω(updatedRelease.TargetCommitish).Should(Equal(github.String("1z22f1"))) 218 | }) 219 | }) 220 | 221 | Context("when set to autogenerate release notes", func() { 222 | BeforeEach(func() { 223 | request.Params.GenerateReleaseNotes = true 224 | }) 225 | // See https://github.com/google/go-github/issues/2444 226 | It("has no effect on updating the existing release", func() { 227 | _, err := command.Run(sourcesDir, request) 228 | Ω(err).ShouldNot(HaveOccurred()) 229 | 230 | Ω(githubClient.UpdateReleaseCallCount()).Should(Equal(1)) 231 | 232 | updatedRelease := githubClient.UpdateReleaseArgsForCall(0) 233 | Ω(*updatedRelease.Name).Should(Equal("v0.3.12")) 234 | Ω(*updatedRelease.Body).Should(Equal("this is a great release")) 235 | Ω(updatedRelease.GenerateReleaseNotes).Should(BeNil()) 236 | }) 237 | }) 238 | }) 239 | 240 | Context("when the release has not already been created", func() { 241 | BeforeEach(func() { 242 | namePath := filepath.Join(sourcesDir, "name") 243 | tagPath := filepath.Join(sourcesDir, "tag") 244 | 245 | file(namePath, "v0.3.12") 246 | file(tagPath, "0.3.12") 247 | 248 | request = resource.OutRequest{ 249 | Params: resource.OutParams{ 250 | NamePath: "name", 251 | TagPath: "tag", 252 | }, 253 | } 254 | }) 255 | 256 | Context("with a commitish", func() { 257 | BeforeEach(func() { 258 | commitishPath := filepath.Join(sourcesDir, "commitish") 259 | file(commitishPath, "a2f4a3") 260 | request.Params.CommitishPath = "commitish" 261 | }) 262 | 263 | It("creates a release on GitHub with the commitish", func() { 264 | _, err := command.Run(sourcesDir, request) 265 | Ω(err).ShouldNot(HaveOccurred()) 266 | 267 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 268 | release := githubClient.CreateReleaseArgsForCall(0) 269 | 270 | Ω(release.TargetCommitish).Should(Equal(github.String("a2f4a3"))) 271 | }) 272 | }) 273 | 274 | Context("without a commitish", func() { 275 | It("creates a release on GitHub without the commitish", func() { 276 | _, err := command.Run(sourcesDir, request) 277 | Ω(err).ShouldNot(HaveOccurred()) 278 | 279 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 280 | release := githubClient.CreateReleaseArgsForCall(0) 281 | 282 | // GitHub treats empty string the same as not suppying the field. 283 | Ω(release.TargetCommitish).Should(Equal(github.String(""))) 284 | }) 285 | }) 286 | 287 | Context("with a body", func() { 288 | BeforeEach(func() { 289 | bodyPath := filepath.Join(sourcesDir, "body") 290 | file(bodyPath, "this is a great release") 291 | request.Params.BodyPath = "body" 292 | }) 293 | 294 | It("creates a release on GitHub", func() { 295 | _, err := command.Run(sourcesDir, request) 296 | Ω(err).ShouldNot(HaveOccurred()) 297 | 298 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 299 | release := githubClient.CreateReleaseArgsForCall(0) 300 | 301 | Ω(*release.Name).Should(Equal("v0.3.12")) 302 | Ω(*release.TagName).Should(Equal("0.3.12")) 303 | Ω(*release.Body).Should(Equal("this is a great release")) 304 | }) 305 | }) 306 | 307 | Context("without a body", func() { 308 | It("works", func() { 309 | _, err := command.Run(sourcesDir, request) 310 | Ω(err).ShouldNot(HaveOccurred()) 311 | 312 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 313 | release := githubClient.CreateReleaseArgsForCall(0) 314 | 315 | Ω(*release.Name).Should(Equal("v0.3.12")) 316 | Ω(*release.TagName).Should(Equal("0.3.12")) 317 | Ω(*release.Body).Should(Equal("")) 318 | }) 319 | }) 320 | 321 | It("always defaults to non-draft mode", func() { 322 | _, err := command.Run(sourcesDir, request) 323 | Ω(err).ShouldNot(HaveOccurred()) 324 | 325 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 326 | release := githubClient.CreateReleaseArgsForCall(0) 327 | 328 | Ω(*release.Draft).Should(Equal(false)) 329 | }) 330 | 331 | Context("when pre-release are set and release are not", func() { 332 | BeforeEach(func() { 333 | bodyPath := filepath.Join(sourcesDir, "body") 334 | file(bodyPath, "this is a great release") 335 | request.Source.Release = false 336 | request.Source.PreRelease = true 337 | }) 338 | 339 | It("creates a non-draft pre-release in Github", func() { 340 | _, err := command.Run(sourcesDir, request) 341 | Ω(err).ShouldNot(HaveOccurred()) 342 | 343 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 344 | release := githubClient.CreateReleaseArgsForCall(0) 345 | 346 | Ω(*release.Name).Should(Equal("v0.3.12")) 347 | Ω(*release.TagName).Should(Equal("0.3.12")) 348 | Ω(*release.Body).Should(Equal("")) 349 | Ω(*release.Draft).Should(Equal(false)) 350 | Ω(*release.Prerelease).Should(Equal(true)) 351 | }) 352 | 353 | It("has some sweet metadata", func() { 354 | outResponse, err := command.Run(sourcesDir, request) 355 | Ω(err).ShouldNot(HaveOccurred()) 356 | 357 | Ω(outResponse.Metadata).Should(ConsistOf( 358 | resource.MetadataPair{Name: "url", Value: "http://google.com"}, 359 | resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"}, 360 | resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true}, 361 | resource.MetadataPair{Name: "tag", Value: "0.3.12"}, 362 | resource.MetadataPair{Name: "pre-release", Value: "true"}, 363 | )) 364 | }) 365 | }) 366 | 367 | Context("when release and pre-release are set", func() { 368 | BeforeEach(func() { 369 | bodyPath := filepath.Join(sourcesDir, "body") 370 | file(bodyPath, "this is a great release") 371 | request.Source.Release = true 372 | request.Source.PreRelease = true 373 | }) 374 | 375 | It("creates a final release in Github", func() { 376 | _, err := command.Run(sourcesDir, request) 377 | Ω(err).ShouldNot(HaveOccurred()) 378 | 379 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 380 | release := githubClient.CreateReleaseArgsForCall(0) 381 | 382 | Ω(*release.Name).Should(Equal("v0.3.12")) 383 | Ω(*release.TagName).Should(Equal("0.3.12")) 384 | Ω(*release.Body).Should(Equal("")) 385 | Ω(*release.Draft).Should(Equal(false)) 386 | Ω(*release.Prerelease).Should(Equal(false)) 387 | }) 388 | 389 | It("has some sweet metadata", func() { 390 | outResponse, err := command.Run(sourcesDir, request) 391 | Ω(err).ShouldNot(HaveOccurred()) 392 | 393 | Ω(outResponse.Metadata).Should(ConsistOf( 394 | resource.MetadataPair{Name: "url", Value: "http://google.com"}, 395 | resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"}, 396 | resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true}, 397 | resource.MetadataPair{Name: "tag", Value: "0.3.12"}, 398 | )) 399 | }) 400 | }) 401 | 402 | Context("when set as a draft release", func() { 403 | BeforeEach(func() { 404 | bodyPath := filepath.Join(sourcesDir, "body") 405 | file(bodyPath, "this is a great release") 406 | request.Source.Drafts = true 407 | }) 408 | 409 | It("creates a release on GitHub in draft mode", func() { 410 | _, err := command.Run(sourcesDir, request) 411 | Ω(err).ShouldNot(HaveOccurred()) 412 | 413 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 414 | release := githubClient.CreateReleaseArgsForCall(0) 415 | 416 | Ω(*release.Name).Should(Equal("v0.3.12")) 417 | Ω(*release.TagName).Should(Equal("0.3.12")) 418 | Ω(*release.Body).Should(Equal("")) 419 | Ω(*release.Draft).Should(Equal(true)) 420 | Ω(*release.Prerelease).Should(Equal(false)) 421 | }) 422 | 423 | It("has some sweet metadata", func() { 424 | outResponse, err := command.Run(sourcesDir, request) 425 | Ω(err).ShouldNot(HaveOccurred()) 426 | 427 | Ω(outResponse.Metadata).Should(ConsistOf( 428 | resource.MetadataPair{Name: "url", Value: "http://google.com"}, 429 | resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"}, 430 | resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true}, 431 | resource.MetadataPair{Name: "tag", Value: "0.3.12"}, 432 | resource.MetadataPair{Name: "draft", Value: "true"}, 433 | )) 434 | }) 435 | }) 436 | 437 | Context("with file globs", func() { 438 | BeforeEach(func() { 439 | globMatching := filepath.Join(sourcesDir, "great-file.tgz") 440 | globNotMatching := filepath.Join(sourcesDir, "bad-file.txt") 441 | 442 | file(globMatching, "matching") 443 | file(globNotMatching, "not matching") 444 | 445 | request = resource.OutRequest{ 446 | Params: resource.OutParams{ 447 | NamePath: "name", 448 | BodyPath: "body", 449 | TagPath: "tag", 450 | 451 | Globs: []string{ 452 | "*.tgz", 453 | }, 454 | }, 455 | } 456 | 457 | bodyPath := filepath.Join(sourcesDir, "body") 458 | file(bodyPath, "*markdown*") 459 | request.Params.BodyPath = "body" 460 | }) 461 | 462 | It("uploads matching file globs", func() { 463 | _, err := command.Run(sourcesDir, request) 464 | Ω(err).ShouldNot(HaveOccurred()) 465 | 466 | Ω(githubClient.UploadReleaseAssetCallCount()).Should(Equal(1)) 467 | release, name, file := githubClient.UploadReleaseAssetArgsForCall(0) 468 | 469 | Ω(*release.ID).Should(Equal(int64(112))) 470 | Ω(name).Should(Equal("great-file.tgz")) 471 | Ω(file.Name()).Should(Equal(filepath.Join(sourcesDir, "great-file.tgz"))) 472 | }) 473 | 474 | It("has some sweet metadata", func() { 475 | outResponse, err := command.Run(sourcesDir, request) 476 | Ω(err).ShouldNot(HaveOccurred()) 477 | 478 | Ω(outResponse.Metadata).Should(ConsistOf( 479 | resource.MetadataPair{Name: "url", Value: "http://google.com"}, 480 | resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"}, 481 | resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true}, 482 | resource.MetadataPair{Name: "tag", Value: "0.3.12"}, 483 | )) 484 | }) 485 | 486 | It("returns an error if a glob is provided that does not match any files", func() { 487 | request.Params.Globs = []string{ 488 | "*.tgz", 489 | "*.gif", 490 | } 491 | 492 | _, err := command.Run(sourcesDir, request) 493 | Ω(err).Should(HaveOccurred()) 494 | Ω(err).Should(MatchError("could not find file that matches glob '*.gif'")) 495 | }) 496 | 497 | Context("when upload release asset fails", func() { 498 | BeforeEach(func() { 499 | existingAsset := false 500 | githubClient.DeleteReleaseAssetStub = func(github.ReleaseAsset) error { 501 | existingAsset = false 502 | return nil 503 | } 504 | 505 | githubClient.ListReleaseAssetsReturns([]*github.ReleaseAsset{ 506 | { 507 | ID: github.Int64(456789), 508 | Name: github.String("great-file.tgz"), 509 | }, 510 | { 511 | ID: github.Int64(3450798), 512 | Name: github.String("whatever.tgz"), 513 | }, 514 | }, nil) 515 | 516 | githubClient.UploadReleaseAssetStub = func(rel github.RepositoryRelease, name string, file *os.File) error { 517 | Expect(io.ReadAll(file)).To(Equal([]byte("matching"))) 518 | Expect(existingAsset).To(BeFalse()) 519 | existingAsset = true 520 | return errors.New("some-error") 521 | } 522 | }) 523 | 524 | It("retries 10 times", func() { 525 | _, err := command.Run(sourcesDir, request) 526 | Expect(err).To(Equal(errors.New("some-error"))) 527 | 528 | Ω(githubClient.UploadReleaseAssetCallCount()).Should(Equal(10)) 529 | Ω(githubClient.ListReleaseAssetsCallCount()).Should(Equal(10)) 530 | Ω(*githubClient.ListReleaseAssetsArgsForCall(9).ID).Should(Equal(int64(112))) 531 | 532 | actualRelease, actualName, actualFile := githubClient.UploadReleaseAssetArgsForCall(9) 533 | Ω(*actualRelease.ID).Should(Equal(int64(112))) 534 | Ω(actualName).Should(Equal("great-file.tgz")) 535 | Ω(actualFile.Name()).Should(Equal(filepath.Join(sourcesDir, "great-file.tgz"))) 536 | 537 | Ω(githubClient.DeleteReleaseAssetCallCount()).Should(Equal(10)) 538 | actualAsset := githubClient.DeleteReleaseAssetArgsForCall(8) 539 | Expect(*actualAsset.ID).To(Equal(int64(456789))) 540 | }) 541 | 542 | Context("when uploading succeeds on the 5th attempt", func() { 543 | BeforeEach(func() { 544 | results := make(chan error, 6) 545 | results <- errors.New("1") 546 | results <- errors.New("2") 547 | results <- errors.New("3") 548 | results <- errors.New("4") 549 | results <- nil 550 | results <- errors.New("6") 551 | 552 | githubClient.UploadReleaseAssetStub = func(github.RepositoryRelease, string, *os.File) error { 553 | return <-results 554 | } 555 | }) 556 | 557 | It("succeeds", func() { 558 | _, err := command.Run(sourcesDir, request) 559 | Expect(err).ToNot(HaveOccurred()) 560 | 561 | Ω(githubClient.UploadReleaseAssetCallCount()).Should(Equal(5)) 562 | Ω(githubClient.ListReleaseAssetsCallCount()).Should(Equal(4)) 563 | Ω(*githubClient.ListReleaseAssetsArgsForCall(3).ID).Should(Equal(int64(112))) 564 | 565 | actualRelease, actualName, actualFile := githubClient.UploadReleaseAssetArgsForCall(4) 566 | Ω(*actualRelease.ID).Should(Equal(int64(112))) 567 | Ω(actualName).Should(Equal("great-file.tgz")) 568 | Ω(actualFile.Name()).Should(Equal(filepath.Join(sourcesDir, "great-file.tgz"))) 569 | 570 | Ω(githubClient.DeleteReleaseAssetCallCount()).Should(Equal(4)) 571 | actualAsset := githubClient.DeleteReleaseAssetArgsForCall(3) 572 | Expect(*actualAsset.ID).To(Equal(int64(456789))) 573 | }) 574 | }) 575 | }) 576 | }) 577 | 578 | Context("when the tag_prefix is set", func() { 579 | BeforeEach(func() { 580 | namePath := filepath.Join(sourcesDir, "name") 581 | tagPath := filepath.Join(sourcesDir, "tag") 582 | 583 | file(namePath, "v0.3.12") 584 | file(tagPath, "0.3.12") 585 | 586 | request = resource.OutRequest{ 587 | Params: resource.OutParams{ 588 | NamePath: "name", 589 | TagPath: "tag", 590 | TagPrefix: "version-", 591 | }, 592 | } 593 | }) 594 | 595 | It("appends the TagPrefix onto the TagName", func() { 596 | _, err := command.Run(sourcesDir, request) 597 | Ω(err).ShouldNot(HaveOccurred()) 598 | 599 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 600 | release := githubClient.CreateReleaseArgsForCall(0) 601 | 602 | Ω(*release.Name).Should(Equal("v0.3.12")) 603 | Ω(*release.TagName).Should(Equal("version-0.3.12")) 604 | }) 605 | }) 606 | 607 | Context("with generate_release_notes set to false", func() { 608 | BeforeEach(func() { 609 | request.Params.GenerateReleaseNotes = false 610 | }) 611 | 612 | It("creates a release on GitHub without autogenerated release notes", func() { 613 | _, err := command.Run(sourcesDir, request) 614 | Ω(err).ShouldNot(HaveOccurred()) 615 | 616 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 617 | release := githubClient.CreateReleaseArgsForCall(0) 618 | 619 | Ω(release.GenerateReleaseNotes).Should(Equal(github.Bool(false))) 620 | }) 621 | }) 622 | 623 | Context("with generate_release_notes set to true", func() { 624 | BeforeEach(func() { 625 | request.Params.GenerateReleaseNotes = true 626 | }) 627 | 628 | It("creates a release on GitHub with autogenerated release notes", func() { 629 | _, err := command.Run(sourcesDir, request) 630 | Ω(err).ShouldNot(HaveOccurred()) 631 | 632 | Ω(githubClient.CreateReleaseCallCount()).Should(Equal(1)) 633 | release := githubClient.CreateReleaseArgsForCall(0) 634 | 635 | Ω(release.GenerateReleaseNotes).Should(Equal(github.Bool(true))) 636 | }) 637 | }) 638 | }) 639 | }) 640 | -------------------------------------------------------------------------------- /resource_suite_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | 8 | resource "github.com/concourse/github-release-resource" 9 | "github.com/google/go-github/v66/github" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func TestGithubReleaseResource(t *testing.T) { 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Github Release Resource Suite") 17 | } 18 | 19 | func newRepositoryRelease(id int, version string) *github.RepositoryRelease { 20 | return &github.RepositoryRelease{ 21 | TagName: github.String(version), 22 | Draft: github.Bool(false), 23 | Prerelease: github.Bool(false), 24 | ID: github.Int64(int64(id)), 25 | } 26 | } 27 | 28 | func newPreReleaseRepositoryRelease(id int, version string) *github.RepositoryRelease { 29 | return &github.RepositoryRelease{ 30 | TagName: github.String(version), 31 | Draft: github.Bool(false), 32 | Prerelease: github.Bool(true), 33 | ID: github.Int64(int64(id)), 34 | } 35 | } 36 | func newDraftRepositoryRelease(id int, version string) *github.RepositoryRelease { 37 | return &github.RepositoryRelease{ 38 | TagName: github.String(version), 39 | Draft: github.Bool(true), 40 | Prerelease: github.Bool(false), 41 | ID: github.Int64(int64(id)), 42 | } 43 | } 44 | 45 | func newDraftWithNilTagRepositoryRelease(id int) *github.RepositoryRelease { 46 | return &github.RepositoryRelease{ 47 | Draft: github.Bool(true), 48 | Prerelease: github.Bool(false), 49 | ID: github.Int64(int64(id)), 50 | } 51 | } 52 | 53 | func exampleTimeStamp(day int) time.Time { 54 | return time.Date(2018, time.January, day, 0, 0, 0, 0, time.UTC) 55 | } 56 | 57 | func newRepositoryReleaseWithCreatedTime(id int, version string, day int) *github.RepositoryRelease { 58 | return &github.RepositoryRelease{ 59 | TagName: github.String(version), 60 | Draft: github.Bool(false), 61 | Prerelease: github.Bool(false), 62 | ID: github.Int64(int64(id)), 63 | CreatedAt: &github.Timestamp{Time: exampleTimeStamp(day)}, 64 | } 65 | } 66 | 67 | func newRepositoryReleaseWithPublishedTime(id int, version string, day int) *github.RepositoryRelease { 68 | return &github.RepositoryRelease{ 69 | TagName: github.String(version), 70 | Draft: github.Bool(false), 71 | Prerelease: github.Bool(false), 72 | ID: github.Int64(int64(id)), 73 | PublishedAt: &github.Timestamp{Time: exampleTimeStamp(day)}, 74 | } 75 | } 76 | 77 | func newRepositoryReleaseWithCreatedAndPublishedTime(id int, version string, createdDay int, publishedDay int) *github.RepositoryRelease { 78 | return &github.RepositoryRelease{ 79 | TagName: github.String(version), 80 | Draft: github.Bool(false), 81 | Prerelease: github.Bool(false), 82 | ID: github.Int64(int64(id)), 83 | CreatedAt: &github.Timestamp{Time: exampleTimeStamp(createdDay)}, 84 | PublishedAt: &github.Timestamp{Time: exampleTimeStamp(publishedDay)}, 85 | } 86 | } 87 | 88 | func newVersionWithTimestamp(id int, tag string, day int) resource.Version { 89 | return resource.Version{ 90 | ID: strconv.Itoa(id), 91 | Tag: tag, 92 | Timestamp: exampleTimeStamp(day), 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /resources.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Source struct { 8 | Owner string `json:"owner"` 9 | Repository string `json:"repository"` 10 | 11 | // Deprecated; use Owner instead 12 | User string `json:"user"` 13 | 14 | GitHubAPIURL string `json:"github_api_url"` 15 | GitHubV4APIURL string `json:"github_v4_api_url"` 16 | GitHubUploadsURL string `json:"github_uploads_url"` 17 | AccessToken string `json:"access_token"` 18 | Drafts bool `json:"drafts"` 19 | PreRelease bool `json:"pre_release"` 20 | Release bool `json:"release"` 21 | Insecure bool `json:"insecure"` 22 | AssetDir bool `json:"asset_dir"` 23 | 24 | TagFilter string `json:"tag_filter"` 25 | OrderBy string `json:"order_by"` 26 | SemverConstraint string `json:"semver_constraint"` 27 | } 28 | 29 | type CheckRequest struct { 30 | Source Source `json:"source"` 31 | Version Version `json:"version"` 32 | } 33 | 34 | func NewCheckRequest() CheckRequest { 35 | res := CheckRequest{} 36 | res.Source.Release = true 37 | return res 38 | } 39 | 40 | func NewOutRequest() OutRequest { 41 | res := OutRequest{} 42 | res.Source.Release = true 43 | return res 44 | } 45 | 46 | func NewInRequest() InRequest { 47 | res := InRequest{} 48 | res.Source.Release = true 49 | return res 50 | } 51 | 52 | type InRequest struct { 53 | Source Source `json:"source"` 54 | Version *Version `json:"version"` 55 | Params InParams `json:"params"` 56 | } 57 | 58 | type InParams struct { 59 | Globs []string `json:"globs"` 60 | IncludeSourceTarball bool `json:"include_source_tarball"` 61 | IncludeSourceZip bool `json:"include_source_zip"` 62 | } 63 | 64 | type InResponse struct { 65 | Version Version `json:"version"` 66 | Metadata []MetadataPair `json:"metadata"` 67 | } 68 | 69 | type OutRequest struct { 70 | Source Source `json:"source"` 71 | Params OutParams `json:"params"` 72 | } 73 | 74 | type OutParams struct { 75 | NamePath string `json:"name"` 76 | BodyPath string `json:"body"` 77 | TagPath string `json:"tag"` 78 | CommitishPath string `json:"commitish"` 79 | TagPrefix string `json:"tag_prefix"` 80 | GenerateReleaseNotes bool `json:"generate_release_notes"` 81 | 82 | Globs []string `json:"globs"` 83 | } 84 | 85 | type OutResponse struct { 86 | Version Version `json:"version"` 87 | Metadata []MetadataPair `json:"metadata"` 88 | } 89 | 90 | type Version struct { 91 | Tag string `json:"tag,omitempty"` 92 | ID string `json:"id"` 93 | Timestamp time.Time `json:"timestamp"` 94 | } 95 | 96 | type MetadataPair struct { 97 | Name string `json:"name"` 98 | Value string `json:"value"` 99 | URL string `json:"url"` 100 | Markdown bool `json:"markdown"` 101 | } 102 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package resource 4 | 5 | import ( 6 | _ "github.com/maxbrunsfeld/counterfeiter/v6" 7 | ) 8 | 9 | // This file imports packages that are used when running go generate, or used 10 | // during the development process but not otherwise depended on by built code. 11 | -------------------------------------------------------------------------------- /versions.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/google/go-github/v66/github" 9 | ) 10 | 11 | var defaultTagFilter = "^v?([^v].*)" 12 | 13 | type versionParser struct { 14 | re *regexp.Regexp 15 | } 16 | 17 | func newVersionParser(filter string) (versionParser, error) { 18 | if filter == "" { 19 | filter = defaultTagFilter 20 | } 21 | re, err := regexp.Compile(filter) 22 | if err != nil { 23 | return versionParser{}, err 24 | } 25 | return versionParser{re: re}, nil 26 | } 27 | 28 | func (vp *versionParser) parse(tag string) string { 29 | matches := vp.re.FindStringSubmatch(tag) 30 | if len(matches) > 0 { 31 | return matches[len(matches)-1] 32 | } 33 | return "" 34 | } 35 | 36 | func getTimestamp(release *github.RepositoryRelease) time.Time { 37 | if release.PublishedAt != nil { 38 | return release.PublishedAt.Time 39 | } else if release.CreatedAt != nil { 40 | return release.CreatedAt.Time 41 | } else { 42 | return time.Time{} 43 | } 44 | } 45 | 46 | func versionFromRelease(release *github.RepositoryRelease) Version { 47 | v := Version{ 48 | ID: strconv.FormatInt(*release.ID, 10), 49 | Timestamp: getTimestamp(release), 50 | } 51 | if release.TagName != nil { 52 | v.Tag = *release.TagName 53 | } 54 | return v 55 | } 56 | --------------------------------------------------------------------------------