Skip to content

Commit

Permalink
feat: support monorepos with release-backed tokens (#86)
Browse files Browse the repository at this point in the history
Allows release-backed tokens to be generated with a monorepo option
if monorepo is enabled, release-backed tokens will look for matching
releases that contain the package-name prefix, e.g., yargs-v1.0.0
sql-v1.0.2, rather than just v1.0.0, v1.0.2.
  • Loading branch information
bcoe committed Nov 20, 2020
1 parent fd05811 commit ddb5421
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 65 deletions.
5 changes: 4 additions & 1 deletion src/lib/datastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ export const savePublishKey = async (
publishKey: string,
packageName?: string,
expiration?: number,
releaseAs2FA?: boolean
releaseAs2FA?: boolean,
monorepo?: boolean
): Promise<{}> => {
if (!config.loginEnabled) {
return Promise.reject(new Error('disabled on this server.'));
Expand All @@ -135,6 +136,7 @@ export const savePublishKey = async (
package: packageName,
expiration,
releaseAs2FA,
monorepo,
},
});
};
Expand Down Expand Up @@ -242,6 +244,7 @@ export interface PublishKey {
package?: string;
expiration?: number;
releaseAs2FA?: boolean;
monorepo?: boolean;
}

interface UserMain {
Expand Down
59 changes: 37 additions & 22 deletions src/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,38 +55,53 @@ export const getRepo = (name: string, token: string): Promise<GhRepo> => {
/**
* https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository
* @param name repository name including username. ex: node/node or bcoe/yargs
* @param prefix if present, it is required that the release tags include the
* prefix, e.g., yargs-v1.0.0. This allows a token to be created that works
* for a monorepo.
* @param token
* @param tag release tag to fetch.
*
* @returns string[] tag names of releases.
*/
export const getRelease = (
export const getRelease = async (
name: string,
token: string,
tag: string
): Promise<string> => {
tag: string,
prefix?: string
): Promise<string | undefined> => {
const client = gh.client(token, clientOptions);
return new Promise((resolve, reject) => {
client.get(
`/repos/${name}/tags`,
{per_page: 100},
(err: Error, code: number, resp: [{name: string}]) => {
if (err) {
return reject(err);
} else if (code !== 200)
return reject(new Error(`unexpected http code = ${code}`));
else if (
!resp.find(item => {
return item.name === tag;
})
) {
return reject(new Error('not found'));
} else {
return resolve(tag);
// We check up to 600 of the most recent tags for a matching release,
// we use a large page size to allow for monorepos with 100s of tags:
const maxPagination = 6;
for (let page = 1; page < maxPagination; page++) {
const tags: [{name: string}] = await new Promise((resolve, reject) => {
client.get(
`/repos/${name}/tags`,
{per_page: 100, page: page},
(err: Error, code: number, resp: [{name: string}]) => {
if (err) {
return reject(Error(`getRelease: tag = ${tag}`));
} else if (code !== 200) {
return reject(
new Error(
`getRelease: unexpected http code = ${code} tag = ${tag}`
)
);
} else {
resolve(resp);
}
}
);
});
for (const item of tags) {
if (!prefix && item.name === tag) {
return tag;
} else if (item.name === `${prefix}-${tag}`) {
return tag;
}
);
});
}
}
return undefined;
};

/**
Expand Down
22 changes: 17 additions & 5 deletions src/lib/write-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ export const writePackage = async (
repo.name,
user.token,
newPackage ? undefined : doc,
drainedBody
drainedBody,
pubKey.monorepo
);
} catch (e) {
return respondWithError(res, e.statusMessage, e.statusCode);
Expand Down Expand Up @@ -231,7 +232,8 @@ async function enforceMatchingRelease(
repoName: string,
token: string,
lastPackument: Packument | undefined,
drainedBody: Buffer
drainedBody: Buffer,
monorepo?: boolean
) {
try {
const maybePackument = JSON.parse(drainedBody + '');
Expand Down Expand Up @@ -277,13 +279,23 @@ async function enforceMatchingRelease(
newVersion = versions[0];
}
}
try {
await github.getRelease(repoName, token, `v${newVersion}`);
} catch (err) {
let prefix;
if (monorepo) {
const splitName = newPackument.name.split('/');
prefix = splitName.length === 1 ? splitName[0] : splitName[1];
}
const release = await github.getRelease(
repoName,
token,
`v${newVersion}`,
prefix
);
if (!release) {
const msg = `matching release v${newVersion} not found for ${repoName}`;
throw new WombatServerError(msg, 400);
}
} catch (err) {
console.error(err);
if (err.statusCode && err.statusMessage) throw err;
err.statusCode = 500;
err.statusMessage = 'unknown error';
Expand Down
3 changes: 2 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,8 @@ app.get(
handoff.value,
req.query.package ? (req.query.package + '').trim() : undefined,
ttl,
releaseAs2FA
releaseAs2FA,
req.query.monorepo === 'on' ? true : false
),
datastore.completeHandoffKey(req.query.ott + ''),
]);
Expand Down
42 changes: 40 additions & 2 deletions test/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('github', () => {
describe('getLatestRelease', () => {
it('returns latest release from GitHub', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/tags?per_page=100')
.get('/repos/bcoe/test/tags?per_page=100&page=1')
.reply(200, [{name: 'v1.0.2'}]);

const latest = await github.getRelease('bcoe/test', 'abc123', 'v1.0.2');
Expand All @@ -35,7 +35,7 @@ describe('github', () => {

it('bubbles error appropriately', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/tags?per_page=100')
.get('/repos/bcoe/test/tags?per_page=100&page=1')
.reply(404);
let err: Error | undefined = undefined;
try {
Expand All @@ -49,5 +49,43 @@ describe('github', () => {
}
request.done();
});

it('does not return latest release without prefix, when prefix used', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/tags?per_page=100&page=1')
.reply(200, [{name: 'v1.0.2'}])
.get('/repos/bcoe/test/tags?per_page=100&page=2')
.reply(200, [{name: 'v1.0.3'}])
.get('/repos/bcoe/test/tags?per_page=100&page=3')
.reply(200, [{name: 'v1.0.4'}])
.get('/repos/bcoe/test/tags?per_page=100&page=4')
.reply(200, [{name: 'v1.0.5'}])
.get('/repos/bcoe/test/tags?per_page=100&page=5')
.reply(200, [{name: 'v1.0.6'}]);

expect(
await github.getRelease('bcoe/test', 'abc123', 'v1.0.2', 'foo')
).to.equal(undefined);
request.done();
});

it('returns latest release matching prefix', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/tags?per_page=100&page=1')
.reply(200, [{name: 'v1.0.3'}])
.get('/repos/bcoe/test/tags?per_page=100&page=2')
.reply(200, [{name: 'v1.0.4'}])
.get('/repos/bcoe/test/tags?per_page=100&page=3')
.reply(200, [{name: 'foo-v1.0.2'}]);

const latest = await github.getRelease(
'bcoe/test',
'abc123',
'v1.0.2',
'foo'
);
expect(latest).to.equal('v1.0.2');
request.done();
});
});
});
Loading

0 comments on commit ddb5421

Please sign in to comment.