From 2ca0f14cb3206acf4c4e01e734761e7e803b89f8 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Thu, 20 Aug 2026 17:00:40 +0530 Subject: [PATCH 1/3] fix(bootstrap): reject on non-2xx tarball response and handle zlib errors streamRelease now throws GithubError for HTTP 4xx/5xx responses instead of silently piping the error body (e.g. "404: Not Found") into the zlib decompressor. This was the root cause of the Z_DATA_ERROR crash when the cli-use branch was absent from a repo. extract now attaches an error handler directly on the zlib.createUnzip() stream. Node's pipe() does not forward stream errors, so without this listener a zlib failure emitted an unhandled error event and crashed the process rather than rejecting the Promise cleanly. Co-Authored-By: Claude Sonnet 4.6 --- .talismanrc | 2 + .../src/bootstrap/github/client.ts | 12 +- .../test/github.test.js | 106 ++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/.talismanrc b/.talismanrc index 7efa0e61f..e61cf055f 100644 --- a/.talismanrc +++ b/.talismanrc @@ -39,4 +39,6 @@ fileignoreconfig: checksum: a64a4d396eddd936a63b799eff58c5c6660b5dcaa3a310fd8b09a027932f1789 - filename: packages/contentstack-migration/README.md checksum: e96006c1a948f766c88ae972b29582fa58eaf8184606bf011eebddc5a06cd7b6 +- filename: packages/contentstack-bootstrap/test/github.test.js + checksum: b7badfcd3bbad0cb876364542bba26cdfd854f1b138be2896b5f84c219767040 version: "" diff --git a/packages/contentstack-bootstrap/src/bootstrap/github/client.ts b/packages/contentstack-bootstrap/src/bootstrap/github/client.ts index bf300bc04..fa91d3735 100644 --- a/packages/contentstack-bootstrap/src/bootstrap/github/client.ts +++ b/packages/contentstack-bootstrap/src/bootstrap/github/client.ts @@ -74,13 +74,23 @@ export default class GitHubClient { } const response = await HttpClient.create().options(options).get(url); + + if (response.status >= 400) { + throw new GithubError( + messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', `${this.repo.user}/${this.repo.name}`), + response.status, + ); + } + return response.data as Stream; } async extract(destination: string, stream: Stream): Promise { return new Promise((resolve, reject) => { + const unzip = zlib.createUnzip(); + unzip.on('error', reject); stream - .pipe(zlib.createUnzip()) + .pipe(unzip) .pipe( tar.extract({ cwd: destination, diff --git a/packages/contentstack-bootstrap/test/github.test.js b/packages/contentstack-bootstrap/test/github.test.js index 8af88949a..d004921a5 100644 --- a/packages/contentstack-bootstrap/test/github.test.js +++ b/packages/contentstack-bootstrap/test/github.test.js @@ -1,5 +1,9 @@ const { expect } = require('chai'); +const sinon = require('sinon'); +const { Readable } = require('stream'); +const { HttpClient } = require('@contentstack/cli-utilities'); const GitHubClient = require('../lib/bootstrap/github/client').default; +const GithubError = require('../lib/bootstrap/github/github-error').default; describe('Github Client', function () { it('Parse github url', () => { @@ -16,4 +20,106 @@ describe('Github Client', function () { 'https://api.github.com/repos/contentstack/contentstack-nextjs-react-universal-demo/tarball/cli-use', ); }); + + describe('streamRelease', function () { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should throw GithubError with status 404 when the branch does not exist', async () => { + const notFoundStream = new Readable({ read() {} }); + notFoundStream.push(Buffer.from('404: Not Found')); + notFoundStream.push(null); + + const httpStub = { get: sandbox.stub().resolves({ status: 404, data: notFoundStream }), options: sandbox.stub().returnsThis() }; + sandbox.stub(HttpClient, 'create').returns(httpStub); + + const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next')); + + try { + await client.streamRelease(client.gitTarBallUrl); + throw new Error('Expected GithubError to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(GithubError); + expect(err.status).to.equal(404); + } + }); + + it('should throw GithubError with status 500 on server error', async () => { + const errStream = new Readable({ read() {} }); + errStream.push(Buffer.from('Internal Server Error')); + errStream.push(null); + + const httpStub = { get: sandbox.stub().resolves({ status: 500, data: errStream }), options: sandbox.stub().returnsThis() }; + sandbox.stub(HttpClient, 'create').returns(httpStub); + + const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next')); + + try { + await client.streamRelease(client.gitTarBallUrl); + throw new Error('Expected GithubError to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(GithubError); + expect(err.status).to.equal(500); + } + }); + + it('should return the response stream when status is 200', async () => { + const mockStream = new Readable({ read() {} }); + const httpStub = { get: sandbox.stub().resolves({ status: 200, data: mockStream }), options: sandbox.stub().returnsThis() }; + sandbox.stub(HttpClient, 'create').returns(httpStub); + + const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next')); + const result = await client.streamRelease(client.gitTarBallUrl); + + expect(result).to.equal(mockStream); + }); + + it('should pass Authorization header for private repos', async () => { + const mockStream = new Readable({ read() {} }); + const httpStub = { get: sandbox.stub().resolves({ status: 200, data: mockStream }), options: sandbox.stub().returnsThis() }; + sandbox.stub(HttpClient, 'create').returns(httpStub); + + const client = new GitHubClient(GitHubClient.parsePath('contentstack/private-repo'), true, 'my-token'); + await client.streamRelease(client.gitTarBallUrl); + + const callOptions = httpStub.options.firstCall.args[0]; + expect(callOptions.headers).to.deep.equal({ Authorization: 'token my-token' }); + }); + + it('should throw GithubError immediately for private repos with no access token', async () => { + const client = new GitHubClient(GitHubClient.parsePath('contentstack/private-repo'), true, undefined); + + try { + await client.streamRelease(client.gitTarBallUrl); + throw new Error('Expected GithubError to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(GithubError); + expect(err.status).to.equal(1); + } + }); + }); + + describe('extract', function () { + it('should reject (not crash the process) when the stream contains invalid gzip data', async () => { + const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next')); + + const badStream = new Readable({ read() {} }); + badStream.push(Buffer.from('404: Not Found')); + badStream.push(null); + + try { + await client.extract('/tmp', badStream); + throw new Error('Expected extraction error to be thrown'); + } catch (err) { + expect(err.code).to.equal('Z_DATA_ERROR'); + } + }); + }); }); From 0d34d713c1ab4513bcc7eefffd240afe280eb777 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Thu, 20 Aug 2026 17:32:59 +0530 Subject: [PATCH 2/3] fix(bootstrap): stop spinner before printing error so message is visible Moving cliux.loader() (spinner stop) out of finally and into catch before cliux.error() prevents the spinner's carriage-return from wiping the error line. Success path stops the spinner inline after getLatest resolves. Co-Authored-By: Claude Sonnet 4.6 --- packages/contentstack-bootstrap/src/bootstrap/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contentstack-bootstrap/src/bootstrap/index.ts b/packages/contentstack-bootstrap/src/bootstrap/index.ts index c68bcdcaa..e8ea62911 100644 --- a/packages/contentstack-bootstrap/src/bootstrap/index.ts +++ b/packages/contentstack-bootstrap/src/bootstrap/index.ts @@ -71,15 +71,15 @@ export default class Bootstrap { try { await this.ghClient.getLatest(this.cloneDirectory); + cliux.loader(); } catch (error) { + cliux.loader(); if (error instanceof GithubError) { if (error.status === 404) { cliux.error(messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', this.appConfig.source)); } } throw error; - } finally { - cliux.loader(); } // seed plugin start From b0839fc57a447c6ed9972f10bbf457249d9540d0 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Thu, 20 Aug 2026 18:06:25 +0530 Subject: [PATCH 3/3] fix(bootstrap): surface precise branch-not-found error to the developer Replace the generic cliux.error+rethrow pattern with a single clean Error throw so oclif prints one message. Message names both the repo and the missing cli-use branch so the developer knows exactly what to check on GitHub. Co-Authored-By: Claude Sonnet 4.6 --- .talismanrc | 2 +- packages/contentstack-bootstrap/messages/index.json | 1 + packages/contentstack-bootstrap/src/bootstrap/index.ts | 6 ++---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.talismanrc b/.talismanrc index e61cf055f..557172718 100644 --- a/.talismanrc +++ b/.talismanrc @@ -4,7 +4,7 @@ fileignoreconfig: - filename: packages/contentstack-audit/test/unit/mock/am-contents/environments/environments.json checksum: ffaaee9269a6e833cd3dbe337ddc5c060cce948519873c7a2777754519a31b52 - filename: pnpm-lock.yaml - checksum: 649aae748e3ae8c157439dc6c7c9ea402cc19b4dab7ca64b7e4d839d1375df53 + checksum: fda8964d238e0d30577ce8f5bf879a2102c69a7f9e83067ab1627b9b1db9d066 - filename: packages/contentstack-audit/src/types/content-types.ts checksum: d16a65415c3184f15a807d58e5858310aeb5633794fc9075e36f89c94da636c3 - filename: packages/contentstack-audit/src/audit-base-command.ts diff --git a/packages/contentstack-bootstrap/messages/index.json b/packages/contentstack-bootstrap/messages/index.json index 3a56ee430..280e2246f 100644 --- a/packages/contentstack-bootstrap/messages/index.json +++ b/packages/contentstack-bootstrap/messages/index.json @@ -4,6 +4,7 @@ "CLI_BOOTSTRAP_GITHUB_ACCESS_NOT_FOUND": "No Github access token found", "CLI_BOOTSTRAP_START_CLONE_APP": "Cloning the selected app", "CLI_BOOTSTRAP_REPO_NOT_FOUND": "Unable to find a repo for \"%s\"", + "CLI_BOOTSTRAP_APP_UNAVAILABLE": "Unable to download \"%s\": branch \"cli-use\" not found. Ensure the branch exists on the GitHub repository.", "CLI_BOOTSTRAP_NO_API_KEY_FOUND": "No API key generated for the stack", "CLI_BOOTSTRAP_STACK_CREATION_FAILED": "Unable to create stack for content \"%s\"", "CLI_BOOTSTRAP_APP_SELECTION_ENQUIRY": "Select an App", diff --git a/packages/contentstack-bootstrap/src/bootstrap/index.ts b/packages/contentstack-bootstrap/src/bootstrap/index.ts index e8ea62911..90f056bbd 100644 --- a/packages/contentstack-bootstrap/src/bootstrap/index.ts +++ b/packages/contentstack-bootstrap/src/bootstrap/index.ts @@ -74,10 +74,8 @@ export default class Bootstrap { cliux.loader(); } catch (error) { cliux.loader(); - if (error instanceof GithubError) { - if (error.status === 404) { - cliux.error(messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', this.appConfig.source)); - } + if (error instanceof GithubError && error.status === 404) { + throw new Error(messageHandler.parse('CLI_BOOTSTRAP_APP_UNAVAILABLE', this.appConfig.source)); } throw error; }