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 dependency badge for Pipenv applications [GithubPipenv] #4096

Merged
merged 12 commits into from
Oct 2, 2019
29 changes: 24 additions & 5 deletions services/github/github-common-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ const contentSchema = Joi.object({
encoding: Joi.equal('base64').required(),
}).required()

async function fetchJsonFromRepo(
async function fetchRepoContent(
serviceInstance,
{ schema, user, repo, branch = 'master', filename }
{ user, repo, branch = 'master', filename }
Copy link
Member Author

Choose a reason for hiding this comment

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

I hadn't realized that Pipfile is TOML but Pipfile.lock is JSON, so I unnecessarily refactored this. However it'd help with #3960 and #4068 (comment) so seems worth keeping.

Copy link
Member

Choose a reason for hiding this comment

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

Nice!

) {
const errorMessages = errorMessagesFor(
`repo not found, branch not found, or ${filename} missing`
Expand All @@ -41,13 +41,32 @@ async function fetchJsonFromRepo(
errorMessages,
})

let decoded
try {
decoded = Buffer.from(content, 'base64').toString('utf-8')
return Buffer.from(content, 'base64').toString('utf-8')
} catch (e) {
throw new InvalidResponse({ prettyMessage: 'undecodable content' })
}
const json = serviceInstance._parseJson(decoded)
} else {
const url = `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${filename}`
return serviceInstance._request({
url,
errorMessages,
})
}
}

async function fetchJsonFromRepo(
serviceInstance,
{ schema, user, repo, branch = 'master', filename }
) {
if (serviceInstance.staticAuthConfigured) {
const buffer = await fetchRepoContent(serviceInstance, {
user,
repo,
branch,
filename,
})
const json = serviceInstance._parseJson(buffer)
return serviceInstance.constructor._validate(json, schema)
} else {
const url = `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${filename}`
Expand Down
5 changes: 2 additions & 3 deletions services/github/github-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ const { InvalidResponse, NotFound } = require('..')
const documentation = `
<p>
If your GitHub badge errors, it might be because you hit GitHub's rate limits.
<br>
You can increase Shields.io's rate limit by
<a href="https://img.shields.io/github-auth">going to this page</a> to add
Shields as a GitHub application on your GitHub account.
<a href="https://img.shields.io/github-auth">adding the Shields GitHub
application</a> using your GitHub account.
</p>
`

Expand Down
202 changes: 202 additions & 0 deletions services/github/github-pipenv.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
'use strict'

const { renderVersionBadge } = require('../version')
const { isLockfile, getDependencyVersion } = require('../pipenv-helpers')
const { addv } = require('../text-formatters')
const { ConditionalGithubAuthV3Service } = require('./github-auth-service')
const { fetchJsonFromRepo } = require('./github-common-fetch')
const { documentation: githubDocumentation } = require('./github-helpers')
const { NotFound } = require('..')

const keywords = ['pipfile']

const documentation = `
<p>
<a href="https://github.com/pypa/pipenv">Pipenv</a> is a dependency
manager for Python which manages a
<a href="https://virtualenv.pypa.io/en/latest/">virtualenv</a> for
projects. It adds/removes packages from your <code>Pipfile</code> as
you install/uninstall packages and generates the ever-important
<code>Pipfile.lock</code>, which can be checked in to source control
in order to produce deterministic builds.
</p>

<p>
The GitHub Pipenv badges are intended for applications using Pipenv
which are hosted on GitHub.
</p>

<p>
When <code>Pipfile.lock</code> is checked in, the <strong>GitHub Pipenv
locked dependency version</strong> badge displays the locked version of
a dependency listed in <code>[packages]</code> or
<code>[dev-packages]</code> (or any of their transitive dependencies).
</p>

<p>
Usually a Python version is specified in the <code>Pipfile</code>, which
<code>pipenv lock</code> then places in <code>Pipfile.lock</code>. The
<strong>GitHub Pipenv Python version</strong> badge displays that version.
</p>

${githubDocumentation}
`

class GithubPipenvLockedPythonVersion extends ConditionalGithubAuthV3Service {
static get category() {
return 'platform-support'
}

static get route() {
return {
base: 'github/pipenv/locked/python-version',
pattern: ':user/:repo/:branch*',
}
}

static get examples() {
return [
{
title: 'GitHub Pipenv locked Python version',
pattern: ':user/:repo',
namedParams: {
user: 'metabolize',
repo: 'rq-dashboard-on-heroku',
},
staticPreview: this.render({ version: '3.7' }),
documentation,
Copy link
Member

Choose a reason for hiding this comment

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

Will it be evident to end users (either from the locked word in the example and/or route) that this is will only work with a lockfile? Would it be helpful to include some additional text in documentation to convey that?

I've never worked with Pipenv so not sure whether that additional info would helpful or just duplicative clutter

Copy link
Member

Choose a reason for hiding this comment

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

I reckon it will probably be OK, but we could change from "GitHub Pipenv locked Python version" to "GitHub Pipenv Python version from Pipfile.lock" (etc) just to be super clear.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea, agreed, I approached this badge more from a developers perspective than usual and was thinking documentation would be helpful.

In the long run some explanatory documentation could be useful on all our badges.

Copy link
Member Author

Choose a reason for hiding this comment

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

I ended up not adding Pipfile.lock to the title, rather writing some pretty extensive docs of what the badge is for, designed for people who don't necessarily have any context.

keywords,
},
{
title: 'GitHub Pipenv locked Python version (branch)',
pattern: ':user/:repo/:branch',
namedParams: {
user: 'metabolize',
repo: 'rq-dashboard-on-heroku',
branch: 'master',
},
staticPreview: this.render({ version: '3.7', branch: 'master' }),
documentation,
keywords,
},
]
}

static get defaultBadgeData() {
return {
label: 'python',
}
}

static render({ version, branch }) {
return renderVersionBadge({
version,
tag: branch,
defaultLabel: 'python',
})
}

async handle({ user, repo, branch }) {
const {
_meta: {
requires: { python_version: version },
},
} = await fetchJsonFromRepo(this, {
schema: isLockfile,
user,
repo,
branch,
filename: 'Pipfile.lock',
})
if (version === undefined) {
throw new NotFound({ prettyMessage: 'version not specified' })
}
return this.constructor.render({ version, branch })
}
}

class GithubPipenvLockedDependencyVersion extends ConditionalGithubAuthV3Service {
static get category() {
return 'dependencies'
}

static get route() {
return {
base: 'github/pipenv/locked/dependency-version',
pattern: ':user/:repo/:kind(dev)?/:packageName/:branch*',
}
}

static get examples() {
return [
{
title: 'GitHub Pipenv locked dependency version',
pattern: ':user/:repo/:kind(dev)?/:packageName',
namedParams: {
user: 'metabolize',
repo: 'rq-dashboard-on-heroku',
packageName: 'flask',
},
staticPreview: this.render({
dependency: 'flask',
version: '1.1.1',
}),
documentation,
keywords: ['python', ...keywords],
},
{
title: 'GitHub Pipenv locked dependency version (branch)',
pattern: ':user/:repo/:kind(dev)?/:packageName/:branch',
namedParams: {
user: 'metabolize',
repo: 'rq-dashboard-on-heroku',
kind: 'dev',
packageName: 'black',
branch: 'master',
},
staticPreview: this.render({ dependency: 'black', version: '19.3b0' }),
documentation,
keywords: ['python', ...keywords],
},
]
}

static get defaultBadgeData() {
return {
label: 'dependency',
}
}

static render({ dependency, version, ref }) {
return {
label: dependency,
message: version ? addv(version) : ref,
color: 'blue',
}
}

async handle({ user, repo, kind, branch, packageName }) {
const lockfileData = await fetchJsonFromRepo(this, {
schema: isLockfile,
user,
repo,
branch,
filename: 'Pipfile.lock',
})
const { version, ref } = getDependencyVersion({
kind,
wantedDependency: packageName,
lockfileData,
})
return this.constructor.render({
dependency: packageName,
version,
ref,
})
}
}

module.exports = [
GithubPipenvLockedPythonVersion,
GithubPipenvLockedDependencyVersion,
]
93 changes: 93 additions & 0 deletions services/github/github-pipenv.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict'

const Joi = require('@hapi/joi')
const { ServiceTester } = require('../tester')
const {
isVPlusDottedVersionAtLeastOne,
isVPlusDottedVersionNClausesWithOptionalSuffix,
} = require('../test-validators')

// e.g. v19.3b0
const isBlackVersion = Joi.string().regex(/^v\d+(\.\d+)*(.*)?$/)
const isShortSha = Joi.string().regex(/[0-9a-f]{7}/)

const t = (module.exports = new ServiceTester({
id: 'GithubPipenv',
title: 'GithubPipenv',
pathPrefix: '/github/pipenv',
}))

t.create('Locked Python version')
.get('/locked/python-version/metabolize/rq-dashboard-on-heroku.json')
.expectBadge({
label: 'python',
message: isVPlusDottedVersionAtLeastOne,
})

t.create('Locked Python version (no pipfile.lock)')
.get('/locked/python-version/metabolize/react-flexbox-svg.json')
.expectBadge({
label: 'python',
message: 'repo not found, branch not found, or Pipfile.lock missing',
})

t.create('Locked Python version (pipfile.lock has no python version)')
.get('/locked/python-version/fikovnik/ShiftIt.json')
.expectBadge({
label: 'python',
message: 'version not specified',
})

t.create('Locked version of default dependency')
.get(
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/rq-dashboard.json'
)
.expectBadge({
label: 'rq-dashboard',
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
})

t.create('Locked version of default dependency (branch)')
.get(
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/rq-dashboard/master.json'
)
.expectBadge({
label: 'rq-dashboard',
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
})

t.create('Locked version of dev dependency')
.get(
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/dev/black.json'
)
.expectBadge({
label: 'black',
message: isBlackVersion,
})

t.create('Locked version of dev dependency (branch)')
.get(
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/dev/black/master.json'
)
.expectBadge({
label: 'black',
message: isBlackVersion,
})

t.create('Locked version of unknown dependency')
.get(
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/dev/i-made-this-up.json'
)
.expectBadge({
label: 'dependency',
message: 'dev dependency not found',
})

t.create('Locked version of VCS dependency')
.get(
'/locked/dependency-version/DemocracyClub/aggregator-api/dc-base-theme.json'
)
.expectBadge({
label: 'dc-base-theme',
message: isShortSha,
})
Loading