├── .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 |
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 |
--------------------------------------------------------------------------------