Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add GitLab Release badge, run all [GitLab] #7021

Merged
merged 8 commits into from Oct 23, 2021
Merged

Conversation

calebcartwright
Copy link
Member

Resolves #4867

@calebcartwright calebcartwright added the service-badge Accepted and actionable changes, features, and bugs label Sep 15, 2021
@shields-cd shields-cd temporarily deployed to shields-staging-pr-7021 September 15, 2021 01:38 Inactive
@shields-ci
Copy link

shields-ci commented Sep 15, 2021

Warnings
⚠️

📚 Remember to ensure any changes to config.private in services/gitlab/gitlab-base.js are reflected in the server secrets documentation

Messages
📖 ✨ Thanks for your contribution to Shields, @calebcartwright!

Generated by 🚫 dangerJS against f32a86e

Comment on lines +31 to +64
async fetchPaginatedArrayData({
url,
options,
schema,
errorMessages,
firstPageOnly = false,
}) {
const requestParams = this.authHelper.withBasicAuth({
url,
options: {
headers: { Accept: 'application/json' },
qs: { per_page: 100 },
...options,
},
errorMessages,
})

const {
res: { headers },
data,
} = await this.fetchPage({ page: 1, requestParams, schema })
const numberOfPages = headers['x-total-pages']

if (numberOfPages === 1 || firstPageOnly) {
return data
}

const pageData = await Promise.all(
[...Array(numberOfPages - 1).keys()].map((_, i) =>
this.fetchPage({ page: ++i + 1, requestParams, schema })
)
)
return [...data].concat(...pageData)
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitLab's max per page value is 100, and I saw that exceeded in several cases just with the releases on the handful of projects I tested with. A cursory glance through the GitLab API docs makes it seem a very common response structure will be pages of arrays with the page keys in the response headers.

Opted to go ahead and pull that into the base class in anticipation of other APIs. Added it with relevant short circuiting logic (since GitLab doesn't seem to have a separate "latest" API endpoint that returns a single object like GitHub does in some cases), and also requesting all the pages in parallel since it's both possible to do so and preferable to 1 by 1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

I know this is a bit of a fiddly thing to write a test for, but if we're going to be relying on this logic in a number of place it would be good to have it under test

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service tests include a target (the GitLab repo itself) which has more than 100 releases so it is observable (albeit manually via TRACE_SERVICES). Probably some class level unit testing is possible with sufficient mocks, but feel like that's probably best investigated/implemented in a separate PR, thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a plan 👍

gitlab_url: optionalUrl,
include_prereleases: Joi.equal(''),
sort: Joi.string().valid('date', 'semver').default('date'),
display_name: Joi.string().valid('tag', 'release').default('tag'),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Figured it made sense to add this from day 0 given where we ended up with this on GitHub. I've opted to set the default to tag to be consistent with what GitHub currently is but don't have strong opinions on this one way or another.

@shields-cd shields-cd temporarily deployed to shields-staging-pr-7021 September 25, 2021 03:01 Inactive

static route = {
base: 'gitlab/v/release',
pattern: ':user/:repo',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably should have picked this up in the GitLab tag PR review, but on Gitlab repos can be nested in subgroups e.g: https://gitlab.com/megabyte-labs/dockerfile/ci-pipeline/ansible-lint (refs #6427 ) . As we start adding a suite of GitLab badges, lets establish the convention that for new API based badges we work with this pattern and don't restirct to only the user/repo format (I think this means allowing an arbitrary number of slashes in this part of the URL). I think this also then implies that any new GitLab badges which take a branch param accept it as a query param ?branch= rather than a URL param (although this possibly depends a bit on the upstream API routes).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two additional wrinkles to consider:

I almost feel like this is going to force us to have a single param where users can either go the easy route and plugin the project id, or the full project path and we'll handle the uri encoding.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so I'm suggesting the pattern we adopt is /:repo+ for the [group(s)]/user/repo and then we pass encodeURIComponent(repo) to the API. Then for any badges where a branch param is valid, we implement that as ?branch=name. I think this pattern of nesting in groups is quite common for gitlab repos and I reckon it is worth allowing the 'friendly' form of the name rather than allowing you to use the repo name for some repositories but requiring the ID for others.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, agreed. Believe bfe7c1c should have this all sorted now, though will need to circle back to how we best apply the pattern to other badges

// https://docs.gitlab.com/ee/api/releases/
return this.fetchPaginatedArrayData({
schema,
url: `${baseUrl}/api/v4/projects/${user}%2F${repo}/releases`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.gitlab.com/ee/api/releases/ says the default ordering is released_at so I guess we are saying ?sort=date corresponds to released_at (as opposed to created_at). Do you have a handle on the difference/tradeoff here? I wonder if it would be best to directly expose both of those options..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a handle on the difference/tradeoff here?

As far as the distinction between the two, my understanding is that it's analogous to GitHub's created_at and published_at fields respectively. The formers are the system derived values set to the true timestamp when the artifact was created, whereas the latters are a customizable field users can leverage to denote a "future" release, which I believe is only available when creating release via the API (for both Lab and Hub)

https://docs.gitlab.com/ee/api/releases/ says the default ordering is released_at so I guess we are saying ?sort=date corresponds to released_at (as opposed to created_at).

I could've sworn I'd left a note on this somewhere but can't find anything anywhere so 🤷‍♂️

In short, I've already found cases where their docs are just wrong as it relates to default ordering (see the Tags endpoint for example), so I don't personally put much stock into their claimed defaults.

The only case where I could potentially see this being relevant would be a scenario where someone is manually creating releases via the API and interleaving dates. That's probably a bit of an edge case, and one I'd prefer to wait to solve for only if someone actually asks, in part because we don't support the same for GitHub and because it introduces scenarios like if/how to account for future released_at/published_at dates.

We'd still have the flexibility down the road to add a new foo query param that could be used to toggle

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. If we reckon the default in the docs may be misleading, should we explicitly pass an order_by param so we're clear which decision we're making them.
Trying to think about it from the point of view of compatibility with the GH badge, I'm now not actually sure what assumption the releases/latest endpoint makes on this.. I guess the reason I'm thinking we might expose both is because if we pick one then inevitably its the wrong one for someone. I'm thinking #6309 , #6236 , etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, we should at a minimum be explicit both in the params we surface on our routes and in what we pass upstream to make us as "future proof" as possible to allow for any changes/enhancements down the line without risking breaking changes.

Believe cc62b85 should cover this, though still want to punt on any considerations of dealing with future date values with the released_at ordering to our future selves 😆

Comment on lines +31 to +64
async fetchPaginatedArrayData({
url,
options,
schema,
errorMessages,
firstPageOnly = false,
}) {
const requestParams = this.authHelper.withBasicAuth({
url,
options: {
headers: { Accept: 'application/json' },
qs: { per_page: 100 },
...options,
},
errorMessages,
})

const {
res: { headers },
data,
} = await this.fetchPage({ page: 1, requestParams, schema })
const numberOfPages = headers['x-total-pages']

if (numberOfPages === 1 || firstPageOnly) {
return data
}

const pageData = await Promise.all(
[...Array(numberOfPages - 1).keys()].map((_, i) =>
this.fetchPage({ page: ++i + 1, requestParams, schema })
)
)
return [...data].concat(...pageData)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

I know this is a bit of a fiddly thing to write a test for, but if we're going to be relying on this logic in a number of place it would be good to have it under test

@shields-cd shields-cd temporarily deployed to shields-staging-pr-7021 October 16, 2021 18:24 Inactive
t.create('Release (project id latest by date)')
.get('/29538796.json')
.expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' })

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an example of a gitlab project which is nested in a group and has releases we could use for a service test: https://gitlab.com/gitlab-org/frontend/eslint-plugin/-/releases

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Excellent find and/or memory 😆

@calebcartwright calebcartwright temporarily deployed to shields-staging-pr-7021 October 16, 2021 19:10 Inactive
@shields-cd shields-cd temporarily deployed to shields-staging-pr-7021 October 17, 2021 16:45 Inactive
@shields-cd shields-cd temporarily deployed to shields-staging-pr-7021 October 17, 2021 17:02 Inactive
@calebcartwright
Copy link
Member Author

Some change on master has caused the tests to start failing, and I presume this is related to the switch from got to node-fetch.

Will try to figure it out and work around it, but wanted to make a note so we'll be sure to test if we flip back from node-fetch to something else

@calebcartwright
Copy link
Member Author

Blocked on a decision/resolution of #7167

@calebcartwright calebcartwright merged commit 50c0be0 into master Oct 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
service-badge Accepted and actionable changes, features, and bugs
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Badge Request: GitLab tags and releases
4 participants