diff --git a/services/github/github-common-fetch.js b/services/github/github-common-fetch.js
index 7defc30804df5..e30a109e3cab1 100644
--- a/services/github/github-common-fetch.js
+++ b/services/github/github-common-fetch.js
@@ -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 }
) {
const errorMessages = errorMessagesFor(
`repo not found, branch not found, or ${filename} missing`
@@ -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}`
diff --git a/services/github/github-helpers.js b/services/github/github-helpers.js
index 18430fdd3a92d..edf44ce9c7646 100644
--- a/services/github/github-helpers.js
+++ b/services/github/github-helpers.js
@@ -7,10 +7,9 @@ const { InvalidResponse, NotFound } = require('..')
const documentation = `
If your GitHub badge errors, it might be because you hit GitHub's rate limits.
-
You can increase Shields.io's rate limit by
- going to this page to add
- Shields as a GitHub application on your GitHub account.
+ adding the Shields GitHub
+ application using your GitHub account.
`
diff --git a/services/github/github-pipenv.service.js b/services/github/github-pipenv.service.js
new file mode 100644
index 0000000000000..eeeae1753df23
--- /dev/null
+++ b/services/github/github-pipenv.service.js
@@ -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 = `
+
+ Pipenv is a dependency
+ manager for Python which manages a
+ virtualenv for
+ projects. It adds/removes packages from your Pipfile
as
+ you install/uninstall packages and generates the ever-important
+ Pipfile.lock
, which can be checked in to source control
+ in order to produce deterministic builds.
+
+
+
+ The GitHub Pipenv badges are intended for applications using Pipenv
+ which are hosted on GitHub.
+
+
+
+ When Pipfile.lock
is checked in, the GitHub Pipenv
+ locked dependency version badge displays the locked version of
+ a dependency listed in [packages]
or
+ [dev-packages]
(or any of their transitive dependencies).
+
+
+
+ Usually a Python version is specified in the Pipfile
, which
+ pipenv lock
then places in Pipfile.lock
. The
+ GitHub Pipenv Python version badge displays that version.
+
+
+${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,
+ 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,
+]
diff --git a/services/github/github-pipenv.tester.js b/services/github/github-pipenv.tester.js
new file mode 100644
index 0000000000000..88019391a8ff5
--- /dev/null
+++ b/services/github/github-pipenv.tester.js
@@ -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,
+ })
diff --git a/services/pipenv-helpers.js b/services/pipenv-helpers.js
new file mode 100644
index 0000000000000..c77b7babf2e7a
--- /dev/null
+++ b/services/pipenv-helpers.js
@@ -0,0 +1,58 @@
+'use strict'
+
+const Joi = require('@hapi/joi')
+const { InvalidParameter } = require('.')
+
+const isDependency = Joi.alternatives(
+ Joi.object({
+ version: Joi.string().required(),
+ }).required(),
+ Joi.object({
+ ref: Joi.string().required(),
+ }).required()
+)
+
+const isLockfile = Joi.object({
+ _meta: Joi.object({
+ requires: Joi.object({
+ python_version: Joi.string(),
+ }).required(),
+ }).required(),
+ default: Joi.object().pattern(Joi.string().required(), isDependency),
+ develop: Joi.object().pattern(Joi.string().required(), isDependency),
+}).required()
+
+function getDependencyVersion({
+ kind = 'default',
+ wantedDependency,
+ lockfileData,
+}) {
+ let dependenciesOfKind
+ if (kind === 'dev') {
+ dependenciesOfKind = lockfileData.develop
+ } else if (kind === 'default') {
+ dependenciesOfKind = lockfileData.default
+ } else {
+ throw Error(`Not very kind: ${kind}`)
+ }
+
+ if (!(wantedDependency in dependenciesOfKind)) {
+ throw new InvalidParameter({
+ prettyMessage: `${kind} dependency not found`,
+ })
+ }
+
+ const { version, ref } = dependenciesOfKind[wantedDependency]
+
+ if (version) {
+ // Strip the `==` which is always present.
+ return { version: version.replace('==', '') }
+ } else {
+ return { ref: ref.substring(1, 8) }
+ }
+}
+
+module.exports = {
+ isLockfile,
+ getDependencyVersion,
+}
diff --git a/services/pypi/pypi-python-versions.tester.js b/services/pypi/pypi-python-versions.tester.js
index b55749d8a5e70..25552ab4f2306 100644
--- a/services/pypi/pypi-python-versions.tester.js
+++ b/services/pypi/pypi-python-versions.tester.js
@@ -3,7 +3,6 @@
const Joi = require('@hapi/joi')
const t = (module.exports = require('../tester').createServiceTester())
-// These regexes are the same, but declared separately for clarity.
const isPipeSeparatedPythonVersions = Joi.string().regex(
/^([0-9]+\.[0-9]+(?: \| )?)+$/
)