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

lib/test: add initial prototype for wiby --test #3

Merged
merged 1 commit into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions bin/wiby.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
#!/usr/bin/env node

require('dotenv').config()
const test = require('../lib/test')
const result = require('../lib/result')

if (process.argv[2] === 'test') {
test()
const args = require('yargs')
.option('test', {
Copy link
Member

Choose a reason for hiding this comment

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

Should we consider test / result to be commands instead? e.g.

wiby test --dependent=[git url or github repo ref or package name from npm] --wait-for-results
wiby result --dependent=[git url or github repo ref or package name from npm]

This does have the downside that you can't --test --result like you do now, but maybe --wait-for-result or smth, which keeps polling for results, is good enough.

The upside is that this allows structuring the project with Yargs.commandDir(). I find that commands are also a nice way to expose and mirror the behavior between a package being a lib and a cli, i.e. require('wiby').test({ dependent: '@pkgjs/nv' }) can be equivalent to npx wiby test --dependent=@pkgjs/nv.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Covered by #7

alias: 't',
describe: 'Test your dependents'
})
.option('result', {
alias: 'r',
describe: 'Get the result of your tests'
})
.argv

if (args.test) {
test(args.test)
}

if (process.argv[2] === 'result') {
if (args.result) {
result()
}

// Usage: wiby --test URL
124 changes: 124 additions & 0 deletions lib/github.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
const { graphql } = require('@octokit/graphql')
const { Octokit } = require('@octokit/rest')
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
})

const graphqlWithAuth = graphql.defaults({
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`
}
})

module.exports.getPackageJson =
async function getPackageJson (owner, repo) {
try {
const resp = await graphqlWithAuth({
query: `query($owner: String!, $repo: String!) {
repository(name: $repo, owner: $owner) {
object(expression: "master:package.json") {
... on Blob {
text
}
}
}
}`,
owner: owner,
repo: repo
})
return (JSON.parse(resp.repository.object.text))
} catch (err) {
if (err.errors && err.errors[0].type === 'NOT_FOUND') {
throw Error(`Could not find GitHub repository at https://www.github.com/${owner}/${repo}`)
} else {
throw err
}
}
}

module.exports.getPermissions =
async function getPermissions (owner, repo) {
try {
const resp = await graphqlWithAuth({
query: `query($owner: String!, $repo: String!) {
repository(name: $repo, owner: $owner) {
viewerPermission
}
}`,
owner: owner,
repo: repo
})
return resp.repository.viewerPermission
} catch (err) {
if (err.errors && err.errors[0].type === 'NOT_FOUND') {
throw Error(`Could not find GitHub repository at https://www.github.com/${owner}/${repo}`)
} else {
throw err
}
}
}

module.exports.getShas =
async function getShas (owner, repo) {
const resp = await octokit.repos.listCommits({
owner,
repo,
per_page: 1
})
const headSha = resp.data[0].sha
const treeSha = resp.data[0].commit.tree.sha
// console.log(`lastSha: ${headSha} treeSha: ${treeSha}`)
return [headSha, treeSha]
}

module.exports.createBlob =
async function createBlob (owner, repo, file) {
const {
data: { sha: blobSha }
} = await octokit.git.createBlob({
owner,
repo,
content: file,
encoding: 'base64'
})
return blobSha
}

module.exports.createTree = async function createTree (owner, repo, treeSha, blobSha) {
const resp = await octokit.git.createTree({
owner,
repo,
base_tree: treeSha,
tree: [
{ path: 'package.json', mode: '100644', sha: blobSha }
],
headers: {
Accept: 'application/json'
}
})
const newTreeSha = resp.data.sha
return newTreeSha
}

module.exports.createCommit =
async function createCommit (owner, repo, message, newTreeSha, headSha) {
const resp = await octokit.git.createCommit({
owner,
repo,
message: message,
tree: newTreeSha,
parents: [headSha]
})
const commitSha = resp.data.sha
return commitSha
}

module.exports.createBranch =
async function createBranch (owner, repo, commitSha, branch) {
await octokit.git.createRef({
owner,
repo,
sha: commitSha,
ref: `refs/heads/${branch}`
})
}
106 changes: 104 additions & 2 deletions lib/test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,105 @@
module.exports = () => {
console.log('wiby test')
const fsPromises = require('fs').promises
const fetch = require('node-fetch')
const github = require('./github')

module.exports = async function (url) {
const dep = await getLocalPackageName()
console.log('Got local package name: ', dep)
const [org, repo] = getOrgRepo(url)
console.log(`Got org: ${org} and repo: ${repo}`)
const packageJSON = await github.getPackageJson(org, repo)
if (!checkPackageInPackageJSON(dep, packageJSON)) {
throw new Error('Dependency not found in package.json')
}
const patch = await getPatch(dep)
console.log('Created patch: ', patch)
const patchedPackageJSON = applyPatch(patch, dep, packageJSON)
await pushPatch(patchedPackageJSON, org, repo, dep)
}

const getCommitHash = module.exports.getCommitHash =
async function getCommitHash (owner, repo) {
const headSha = (await github.getShas(owner, repo))[0]
return headSha
}

const checkPackageInPackageJSON = module.exports.checkPackageInPackageJSON =
function checkPackageInPackageJSON (dep, packageJSON) {
return Object.prototype.hasOwnProperty.call(packageJSON.dependencies, dep)
}

const getOrgRepo = module.exports.getOrgRepo =
function getOrgRepo (url) {
const repoOrgArr = (url.split('github.com'))[1].split('/')
Copy link
Member

Choose a reason for hiding this comment

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

I find https://www.npmjs.com/package/git-url-parse handy for things like this, esp. if we're going to support Github Enterprise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Covered by #10

const org = repoOrgArr[1]
const repo = repoOrgArr[2]
return [org, repo]
}

const getPatch = module.exports.getPatch =
async function getPatch (dep, hash) {
dep = dep || await getLocalPackageName()
const [org, repo] = await getGitHubOrgRepo(dep)
hash = hash || await getCommitHash(org, repo)
const patch = `${org}/${repo}#${hash}`
return patch
}

const getLocalPackageName = module.exports.getLocalPackageName =
async function getLocalPackageName (pkgPath) {
pkgPath = pkgPath || 'package.json'
let pkg = await fsPromises.readFile(pkgPath).catch((err) => {
throw (err)
})
pkg = JSON.parse(pkg)
return pkg.name
}

const getGitHubOrgRepo = module.exports.getGitHubOrgRepo =
async function getGitHubOrgRepo (dep) {
const urlRegex = /github.com\/([^/])+\/[^/]+/g
const resp = await fetchRegistryInfo(dep)
let org, repo
if (resp.repository && resp.repository.url) {
Copy link
Member

Choose a reason for hiding this comment

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

resp.repository can also be a string (the URL). And in this case git-url-parse is definitely handy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Covered by #10

let gitUrl = (resp.repository.url).match(urlRegex)
if (gitUrl) {
gitUrl = gitUrl[0].replace(/(\.git)/g, '')
org = getOrgRepo(gitUrl)[0]
repo = getOrgRepo(gitUrl)[1]
}
}
if (!org && !repo) {
org = 'undefined'
repo = 'undefined'
}
return [org, repo]
}

const fetchRegistryInfo = module.exports.fetchRegistryInfo =
async function fetchRegistryInfo (dep) {
const resp = await fetch(`https://registry.npmjs.org/${dep}`)
Copy link
Member

Choose a reason for hiding this comment

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

We should use pacote for this, so that it can support custom registries. Happy to address this as part of #6.

return resp.json()
}

const applyPatch = module.exports.applyPatch =
function applyPatch (patch, dep, packageJSON) {
if (!Object.prototype.hasOwnProperty.call(packageJSON.dependencies, dep)) {
throw new Error('Dependency not found in package.json')
}
packageJSON.dependencies[dep] = patch
return packageJSON
}

async function pushPatch (packageJSON, owner, repo, dep) {
const file = JSON.stringify(packageJSON, null, 2) + '\n' // assumes package.json is using two spaces
const encodedFile = Buffer.from(file).toString('base64')
const message = `wiby: update ${dep}`
const branch = `wiby-${dep}`
Copy link
Member

Choose a reason for hiding this comment

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

Does dep need sanitization here? Is @ an allowed character in a git branch (in case dep is a scoped package).

Should the branch and message also include some ref back to the hash / branch / PR of the dep being tested?


const [headSha, treeSha] = await github.getShas(owner, repo)
const blobSha = await github.createBlob(owner, repo, encodedFile)
const newTreeSha = await github.createTree(owner, repo, treeSha, blobSha)
const commitSha = await github.createCommit(owner, repo, message, newTreeSha, headSha)
await github.createBranch(owner, repo, commitSha, branch)
Copy link
Member

Choose a reason for hiding this comment

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

I haven't looked in detail, but I think it might be possible to do all of the above in a single API request: https://octokit.github.io/rest.js/v18#repos-create-or-update-file-contents.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Covered by #9

console.log(`Changes pushed to https://github.com/${owner}/${repo}/blob/${branch}/package.json`)
}
Loading