Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 8 additions & 1 deletion .evergreen/verify-packaged-artifact.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ verify_using_gpg() {
verify_using_powershell() {
echo "Verifying $1 using powershell"
powershell Get-AuthenticodeSignature -FilePath $ARTIFACTS_DIR/$1 > "$TMP_FILE" 2>&1

# Get-AuthenticodeSignature just outputs text, it doesn't exit with a non-zero
# code if the file is not signed
if grep -q NotSigned "$TMP_FILE"; then
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is unrelated to MONGOSH-2059, but I noticed we don't correctly validate the signature of the package on windows, so figured I'll fix it as a drive by. Happy to move to a different PR if folks prefer a cleaner separation of changes.

echo "File $1 is not signed"
exit 1
fi
}

verify_using_codesign() {
Expand Down Expand Up @@ -91,4 +98,4 @@ else
(cd "$ARTIFACTS_DIR" && bash "$BASEDIR/retry-with-backoff.sh" curl -sSfLO --url "$(cat "$ARTIFACT_URL_FILE").sig")
verify_using_gpg $ARTIFACT_FILE_NAME
fi
fi
fi
28 changes: 11 additions & 17 deletions packages/build/src/homebrew/publish-to-homebrew.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('HomebrewPublisher', function () {
let homebrewCore: GithubRepo;
let homebrewCoreFork: GithubRepo;
let createPullRequest: sinon.SinonStub;
let httpsSha256: sinon.SinonStub;
let npmPackageSha256: sinon.SinonStub;
let generateFormula: sinon.SinonStub;
let updateHomebrewFork: sinon.SinonStub;

Expand Down Expand Up @@ -41,7 +41,7 @@ describe('HomebrewPublisher', function () {
homebrewCoreFork,
});

httpsSha256 = sinon.stub(testPublisher, 'httpsSha256');
npmPackageSha256 = sinon.stub(testPublisher, 'npmPackageSha256');
generateFormula = sinon.stub(testPublisher, 'generateFormula');
updateHomebrewFork = sinon.stub(testPublisher, 'updateHomebrewFork');
};
Expand All @@ -61,11 +61,9 @@ describe('HomebrewPublisher', function () {
isDryRun: false,
});

httpsSha256
npmPackageSha256
.rejects()
.withArgs(
'https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-1.0.0.tgz'
)
.withArgs('https://registry.npmjs.org/@mongosh/cli-repl/1.0.0')
.resolves('sha');

generateFormula
Expand Down Expand Up @@ -97,7 +95,7 @@ describe('HomebrewPublisher', function () {

await testPublisher.publish();

expect(httpsSha256).to.have.been.called;
expect(npmPackageSha256).to.have.been.called;
expect(generateFormula).to.have.been.called;
expect(updateHomebrewFork).to.have.been.called;
expect(createPullRequest).to.have.been.called;
Expand All @@ -110,11 +108,9 @@ describe('HomebrewPublisher', function () {
isDryRun: false,
});

httpsSha256
npmPackageSha256
.rejects()
.withArgs(
'https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-1.0.0.tgz'
)
.withArgs('https://registry.npmjs.org/@mongosh/cli-repl/1.0.0')
.resolves('sha');

generateFormula
Expand All @@ -136,18 +132,16 @@ describe('HomebrewPublisher', function () {

await testPublisher.publish();

expect(httpsSha256).to.have.been.called;
expect(npmPackageSha256).to.have.been.called;
expect(generateFormula).to.have.been.called;
expect(updateHomebrewFork).to.have.been.called;
expect(createPullRequest).to.not.have.been.called;
});

it('silently ignores an error while deleting the PR branch', async function () {
httpsSha256
npmPackageSha256
.rejects()
.withArgs(
'https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-1.0.0.tgz'
)
.withArgs('https://registry.npmjs.org/@mongosh/cli-repl/1.0.0')
.resolves('sha');

generateFormula
Expand Down Expand Up @@ -179,7 +173,7 @@ describe('HomebrewPublisher', function () {

await testPublisher.publish();

expect(httpsSha256).to.have.been.called;
expect(npmPackageSha256).to.have.been.called;
expect(generateFormula).to.have.been.called;
expect(updateHomebrewFork).to.have.been.called;
expect(createPullRequest).to.have.been.called;
Expand Down
12 changes: 6 additions & 6 deletions packages/build/src/homebrew/publish-to-homebrew.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { GithubRepo } from '@mongodb-js/devtools-github-repo';
import { generateUpdatedFormula as generateUpdatedFormulaFn } from './generate-formula';
import { updateHomebrewFork as updateHomebrewForkFn } from './update-homebrew-fork';
import { httpsSha256 as httpsSha256Fn } from './utils';
import { npmPackageSha256 as npmPackageSha256Fn } from './utils';

export type HomebrewPublisherConfig = {
homebrewCore: GithubRepo;
Expand All @@ -12,19 +12,19 @@ export type HomebrewPublisherConfig = {
};

export class HomebrewPublisher {
readonly httpsSha256: typeof httpsSha256Fn;
readonly npmPackageSha256: typeof npmPackageSha256Fn;
readonly generateFormula: typeof generateUpdatedFormulaFn;
readonly updateHomebrewFork: typeof updateHomebrewForkFn;

constructor(
public config: HomebrewPublisherConfig,
{
httpsSha256 = httpsSha256Fn,
npmPackageSha256 = npmPackageSha256Fn,
generateFormula = generateUpdatedFormulaFn,
updateHomebrewFork = updateHomebrewForkFn,
} = {}
) {
this.httpsSha256 = httpsSha256;
this.npmPackageSha256 = npmPackageSha256;
this.generateFormula = generateFormula;
this.updateHomebrewFork = updateHomebrewFork;
}
Expand All @@ -38,10 +38,10 @@ export class HomebrewPublisher {
githubReleaseLink,
} = this.config;

const cliReplPackageUrl = `https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-${packageVersion}.tgz`;
const cliReplPackageUrl = `https://registry.npmjs.org/@mongosh/cli-repl/${packageVersion}`;
const packageSha = isDryRun
? `dryRun-fakesha256-${Date.now()}`
: await this.httpsSha256(cliReplPackageUrl);
: await this.npmPackageSha256(cliReplPackageUrl);

const homebrewFormula = await this.generateFormula(
{ version: packageVersion, sha: packageSha },
Expand Down
91 changes: 86 additions & 5 deletions packages/build/src/homebrew/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,96 @@
import { expect } from 'chai';
import { httpsSha256 } from './utils';
import { npmPackageSha256 } from './utils';
import sinon from 'sinon';
import crypto from 'crypto';

describe('Homebrew utils', function () {
describe('httpsSha256', function () {
describe('npmPackageSha256', function () {
it('computes the correct sha', async function () {
const url =
'https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-0.6.1.tgz';
const url = 'https://registry.npmjs.org/@mongosh/cli-repl/0.6.1';
const expectedSha =
'3721ea662cd3775373d4d70f7593993564563d9379704896478db1d63f6c8470';

expect(await httpsSha256(url)).to.equal(expectedSha);
expect(await npmPackageSha256(url)).to.equal(expectedSha);
});

describe('when response sha mismatches', function () {
const fakeTarball = Buffer.from('mongosh-2.4.2.tgz');
const fakeTarballShasum = crypto
.createHash('sha1')
.update(fakeTarball)
.digest('hex');

it('retries', async function () {
const httpGet = sinon.stub();
httpGet
.withArgs(
'https://registry.npmjs.org/@mongosh/cli-repl/2.4.2',
'json'
)
.resolves({
dist: {
tarball:
'https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-2.4.2.tgz',
shasum: fakeTarballShasum,
},
});

httpGet
.withArgs(
'https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-2.4.2.tgz',
'binary'
)
.onFirstCall()
.resolves(Buffer.from('mongosh-2.4.2-incomplete.tgz')) // Simulate incomplete/wrong binary download
.onSecondCall()
.resolves(fakeTarball);

const sha = await npmPackageSha256(
'https://registry.npmjs.org/@mongosh/cli-repl/2.4.2',
httpGet
);

expect(sha).to.equal(
crypto.createHash('sha256').update(fakeTarball).digest('hex')
);
});

it('throws if retries are exhausted', async function () {
const httpGet = sinon.stub();
httpGet
.withArgs(
'https://registry.npmjs.org/@mongosh/cli-repl/2.4.2',
'json'
)
.resolves({
dist: {
tarball:
'https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-2.4.2.tgz',
shasum: fakeTarballShasum,
},
});

httpGet
.withArgs(
'https://registry.npmjs.org/@mongosh/cli-repl/-/cli-repl-2.4.2.tgz',
'binary'
)
.resolves(Buffer.from('mongosh-2.4.2-incomplete.tgz')); // Simulate incomplete/wrong binary download

const incompleteTarballShasum = crypto
.createHash('sha1')
.update(Buffer.from('mongosh-2.4.2-incomplete.tgz'))
.digest('hex');

const err = await npmPackageSha256(
'https://registry.npmjs.org/@mongosh/cli-repl/2.4.2',
httpGet
).catch((e) => e);

expect(err.message).to.equal(
`shasum mismatch: expected '${fakeTarballShasum}', got '${incompleteTarballShasum}'`
);
});
});
});
});
69 changes: 64 additions & 5 deletions packages/build/src/homebrew/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,72 @@
import crypto from 'crypto';
import https from 'https';

export function httpsSha256(url: string): Promise<string> {
return new Promise((resolve, reject) => {
export async function npmPackageSha256(
packageUrl: string,
httpGetFn: typeof httpGet = httpGet
): Promise<string> {
const json = await httpGetFn(packageUrl, 'json');
const tarballUrl = json.dist.tarball;
const shasum = json.dist.shasum;

const tarball = await getTarballWithRetries(tarballUrl, shasum, httpGetFn);
const hash = crypto.createHash('sha256');
hash.update(tarball);
return hash.digest('hex');
}

async function getTarballWithRetries(
url: string,
shasum: string,
httpGetFn: typeof httpGet,
attempts = 3
): Promise<Buffer> {
try {
const tarball = await httpGetFn(url, 'binary');
const hash = crypto.createHash('sha1').update(tarball).digest('hex');
if (hash !== shasum) {
throw new Error(`shasum mismatch: expected '${shasum}', got '${hash}'`);
}

return tarball;
} catch (err) {
if (attempts === 0) {
throw err;
}

return getTarballWithRetries(url, shasum, httpGetFn, attempts - 1);
}
}

export function httpGet(url: string, response: 'json'): Promise<any>;
export function httpGet(url: string, response: 'binary'): Promise<Buffer>;

export async function httpGet(
url: string,
responseType: 'json' | 'binary'
): Promise<any | Buffer> {
const response = await new Promise<string | Buffer[]>((resolve, reject) => {
https.get(url, (stream) => {
const hash = crypto.createHash('sha256');
if (responseType === 'json') {
stream.setEncoding('utf8');
}

let data: string | Buffer[] = responseType === 'json' ? '' : [];
stream.on('error', (err) => reject(err));
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('data', (chunk) => {
if (typeof data === 'string') {
data += chunk;
} else {
data.push(chunk);
}
});
stream.on('end', () => resolve(data));
});
});

if (typeof response === 'string') {
return JSON.parse(response);
}

return Buffer.concat(response);
}