Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d1aaa7e
Merge pull request #350 from contentstack/main
maheshmotkar-cs Aug 13, 2026
a0fbdf2
fix(import): clarify global field reference-field warning message
harshitha-cstk Aug 18, 2026
aa7f63f
fix(bulk-operations): publish only to scoped targets
Aug 19, 2026
2ca0f14
fix(bootstrap): reject on non-2xx tarball response and handle zlib er…
cs-raj Aug 20, 2026
0d34d71
fix(bootstrap): stop spinner before printing error so message is visible
cs-raj Aug 20, 2026
b0839fc
fix(bootstrap): surface precise branch-not-found error to the developer
cs-raj Aug 20, 2026
a816e31
version bump
harshitha-cstk Aug 24, 2026
2264a01
fix(tsgen): resolve the real GraphQL endpoint per region
Aug 24, 2026
d350a10
fix(bootstrap): address Copilot review comments
cs-raj Aug 24, 2026
28f9ef3
fix(bootstrap): widen 404 message and guard non-2xx responses
cs-raj Aug 24, 2026
4fb99ed
Merge pull request #351 from contentstack/fix/dx-10153-global-field-r…
harshitha-cstk Aug 25, 2026
c2a9dc1
snyk fix
Aug 26, 2026
7402cf6
Merge branch 'development' into fix/DX-10257
cs-raj Aug 27, 2026
fc48e23
fix(bootstrap): tighten HTTP guard to reject non-2xx and add 302 test
cs-raj Aug 27, 2026
ca06adf
chore(bootstrap): commit pnpm-lock.yaml with sinon test dependency
cs-raj Aug 27, 2026
bbf2569
lockfile update
cs-raj Aug 27, 2026
6c81551
fix(bootstrap): add sinon to devDependencies explicitly
cs-raj Aug 27, 2026
8e61a18
lockfile update
cs-raj Aug 27, 2026
9f6ecea
Merge pull request #361 from contentstack/snyk-fix
naman-contentstack Aug 27, 2026
bbd0d84
Merge branch 'development' into fix/DX-9484
Aug 27, 2026
2d45ccc
Merge pull request #354 from contentstack/fix/DX-9484
naman-contentstack Aug 27, 2026
475a0b8
Merge branch 'development' into fix/DX-10294
Aug 27, 2026
836f17b
Merge pull request #358 from contentstack/fix/DX-10294
naman-contentstack Aug 27, 2026
b4539b1
Merge branch 'development' into fix/DX-10257
cs-raj Aug 27, 2026
1125b7e
Merge pull request #356 from contentstack/fix/DX-10257
cs-raj Aug 27, 2026
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
52 changes: 14 additions & 38 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,42 +1,18 @@
fileignoreconfig:
- filename: packages/contentstack-audit/test/unit/mock/am-contents-no-stack/environments/environments.json
checksum: fe82c708f5f4d0b05694a601855fb7950661048eef8d7f9f794713e3a665064e
- filename: packages/contentstack-audit/test/unit/mock/am-contents/environments/environments.json
checksum: ffaaee9269a6e833cd3dbe337ddc5c060cce948519873c7a2777754519a31b52
- filename: packages/contentstack-bulk-operations/src/utils/backup-dir-asset-fetcher.ts
checksum: fbf1ef882bf4ea8f06a7a28b9248a07e01f5a5d2d4da98dbb9ee52bc16ce318a
- filename: packages/contentstack-bulk-operations/test/unit/utils/backup-dir-asset-fetcher.test.ts
checksum: b905524c952aff0f089fa5347e0db18479b75a438cd9ae77e2dfe1cb203d7e59
- filename: pnpm-lock.yaml
checksum: 649aae748e3ae8c157439dc6c7c9ea402cc19b4dab7ca64b7e4d839d1375df53
- filename: packages/contentstack-audit/src/types/content-types.ts
checksum: d16a65415c3184f15a807d58e5858310aeb5633794fc9075e36f89c94da636c3
- filename: packages/contentstack-audit/src/audit-base-command.ts
checksum: b706ea114a3cb5e4c0e7e163f32ceffa3356a6b0766bf5b8fe387c3c72f44c36
- filename: packages/contentstack-audit/test/unit/mock/am-contents-no-stack/spaces/space_one/assets/chunk0-assets.json
checksum: a276dc984efd49c097b4c38a02a17cfd1d28d704bd403d65af2b7673b8e9dbe9
- filename: packages/contentstack-audit/src/modules/entries.ts
checksum: 13072df0872f8c098d23f49b02f9e8f3e492ab2f483d67a5ea5e423ab406ad94
- filename: packages/contentstack-audit/src/modules/assets.ts
checksum: 9cdc568453a3926d866de4d3d56e6fbc4629eafcaca1c51536c11d937a0bf126
- filename: packages/contentstack-audit/test/unit/modules/assets.test.ts
checksum: 203bb83afa84a696b84066dc6959712289e9fc4135c8cba641f9ff634f32b3db
- filename: packages/contentstack-audit/test/unit/modules/entries.test.ts
checksum: 31d9767d87e4e9db54f641e8cb9d3360f591acdaa8b089a83eaa4aacdf0c3a35
- filename: packages/contentstack-audit/test/unit/mock/am-contents/spaces/space_clean/assets/chunk0-assets.json
checksum: f02886c04ea1097ebc5965fa9a63a878adb9437d56a5b1797be1967fae35f9e4
checksum: 65d0adc160e75d17f5c8de732d554d352fefdc30751509b9ec4c8b2d250e3e2e
- filename: packages/contentstack-bulk-operations/test/unit/utils/revert-retry-handler.test.ts
checksum: 142e7b67a06bc13b6b4191b17f2ea8ae31a3bce20086ea35a92f3e3671eb603d
- filename: packages/contentstack-bulk-operations/src/utils/batch-helper.ts
checksum: ddcd8ef7c56d1122df88075883a2768a28813147e8fdef5b8e72a1ec118c045a
- filename: packages/contentstack-import/src/import/modules/assets.ts
checksum: d213265909a1b163ce733b9e2534e3a74304a2589c2d85e059f10fa0acfdee11
- filename: packages/contentstack-import/src/utils/import-config-handler.ts
checksum: 5b2050b283ea3d990e8ad3207b054c6932b76d2bd82a9f659b287ef11dec9f83
- filename: packages/contentstack-audit/test/unit/mock/am-contents/spaces/space_cross_only/assets/chunk0-assets.json
checksum: 27d5398f5d81bd3af53da9a5d10475a36c08e8fd8ad29df24e8f267494419a20
- filename: packages/contentstack-audit/test/unit/mock/am-contents/spaces/space_one/assets/chunk0-assets.json
checksum: 9fc8b266ef58ecce8d3c4182632d1f7d088dfbce81b3fcea46b087cde3129b91
- filename: packages/contentstack-audit/test/unit/mock/am-contents/stack/stack.json
checksum: c4ac3ff80eb38b1c58eb0db21a232bd84f856d4647093542296199036945083c
checksum: 014f612c3e8db21c891da0c17a1a3b4cd340c026c58034791d86cd5f73aa9f90
- filename: packages/contentstack-cli-tsgen/src/lib/helper.ts
checksum: cc2f88294ca026c29ca44ee7f9994ebc64e56d9a7a015c64070aaf271f1c3ba2
- filename: packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-assets.ts
checksum: 60f33657cc6e2664c17397a2f6b4b54a3f7bf71618931d63cd2811822bd4837e
- filename: packages/contentstack-bulk-operations/src/utils/data-dir-asset-fetcher.ts
checksum: ddcf4601ac47be300eba0eb901e4d45d748c2c4eab676c56677cb9a802fe3db0
- filename: packages/contentstack-migration/src/commands/cm/stacks/migration.ts
checksum: a64a4d396eddd936a63b799eff58c5c6660b5dcaa3a310fd8b09a027932f1789
- filename: packages/contentstack-migration/README.md
checksum: e96006c1a948f766c88ae972b29582fa58eaf8184606bf011eebddc5a06cd7b6
version: ""
checksum: 9392511b337c361b86c36eb2431d8fea9129a61f6b7d70dcb4a633fea339ca61
version: ""
2 changes: 2 additions & 0 deletions packages/contentstack-bootstrap/messages/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"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\": the repository or branch \"cli-use\" was not found. Ensure both exist on GitHub.",
"CLI_BOOTSTRAP_GITHUB_SERVER_ERROR": "Failed to download \"%s\": GitHub returned HTTP %s. Please try again later.",
"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",
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-bootstrap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"mocha": "10.8.2",
"nyc": "^15.1.0",
"oclif": "^4.23.27",
"sinon": "^21.1.2",
"tmp": "^0.2.7",
"ts-node": "^8.10.2",
"typescript": "^5.9.3"
Expand Down
13 changes: 12 additions & 1 deletion packages/contentstack-bootstrap/src/bootstrap/github/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,24 @@ export default class GitHubClient {
}

const response = await HttpClient.create().options(options).get(url);

if (response.status < 200 || response.status >= 300) {
const message = response.status === 404
? messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', `${this.repo.user}/${this.repo.name}`)
: messageHandler.parse('CLI_BOOTSTRAP_GITHUB_SERVER_ERROR', `${this.repo.user}/${this.repo.name}`, response.status);
throw new GithubError(message, response.status);
}

return response.data as Stream;
}

async extract(destination: string, stream: Stream): Promise<any> {
return new Promise((resolve, reject) => {
const unzip = zlib.createUnzip();
stream.on('error', reject);
unzip.on('error', reject);
stream
.pipe(zlib.createUnzip())
.pipe(unzip)
.pipe(
tar.extract({
cwd: destination,
Expand Down
10 changes: 4 additions & 6 deletions packages/contentstack-bootstrap/src/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,13 @@ export default class Bootstrap {

try {
await this.ghClient.getLatest(this.cloneDirectory);
cliux.loader();
} catch (error) {
if (error instanceof GithubError) {
if (error.status === 404) {
cliux.error(messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', this.appConfig.source));
}
cliux.loader();
if (error instanceof GithubError && error.status === 404) {
throw new Error(messageHandler.parse('CLI_BOOTSTRAP_APP_UNAVAILABLE', this.appConfig.source));
}
throw error;
} finally {
cliux.loader();
}

// seed plugin start
Expand Down
124 changes: 124 additions & 0 deletions packages/contentstack-bootstrap/test/github.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -16,4 +20,124 @@ 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 throw GithubError with status 302 on unexpected redirect', async () => {
const redirectStream = new Readable({ read() {} });
redirectStream.push(null);

const httpStub = { get: sandbox.stub().resolves({ status: 302, data: redirectStream }), 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(302);
}
});

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');
}
});
});
});
17 changes: 10 additions & 7 deletions packages/contentstack-bulk-operations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ Bulk operations for assets: publish/unpublish/cross-publish (CMS) and delete/mov

```
USAGE
$ csdx cm:stacks:bulk-assets [-a <value>] [-k <value>] [--operation publish|unpublish|delete|move] [--environments
<value>...] [--locales <value>...] [--source-env <value>] [--source-alias <value>] [--publish-mode bulk|single]
[--branch <value>] [-c <value>] [-y] [--retry-failed <value>] [--revert <value>] [--bulk-operation-file <value>]
[--folder-uid <value>] [-d <value>] [--dry-run] [--space-uid <value>] [--org-uid <value>] [--workspace <value>]
[--asset-uids-file <value>] [--locale <value>] [--target-folder-uid <value>]
$ csdx cm:stacks:bulk-assets [-a <value>] [-k <value>] [--operation publish|unpublish|delete|move] [--source-alias
<value>] [--publish-mode bulk|single] [--branch <value>] [-c <value>] [-y] [--retry-failed <value>] [--revert
<value>] [--bulk-operation-file <value>] [--backup-dir <value> | --source-env <value> | --folder-uid <value> |
--environments <value>... | --locales <value>...] [--dry-run] [--space-uid <value>] [--org-uid <value>] [--workspace
<value>] [--asset-uids-file <value>] [--locale <value>] [--target-folder-uid <value>]

FLAGS
-a, --alias=<value> Uses the name of a saved Management Token to authenticate the command. The command
Expand All @@ -54,13 +54,16 @@ FLAGS
-c, --config=<value> (optional) Specifies the path to a JSON configuration file that defines the options
for the command. Use this file instead of passing multiple CLI flags for a single
run.
-d, --data-dir=<value> Path to exported content folder containing asset publish details.
-k, --stack-api-key=<value> API key of the source stack. You must use either the --stack-api-key flag or the
--alias flag.
-y, --yes Skips interactive confirmation prompts and runs the command immediately using the
provided options. Useful for automation and scripts.
--asset-uids-file=<value> Path to UTF-8 JSON file: exactly `{ "uids": ["uid1", "uid2"] }` (non-empty string
array, no trimming; large lists: see docs for NODE_OPTIONS)
--backup-dir=<value> Path to the import backup directory. Each imported asset is published only to the
environments and locales it was published to in the source stack (read from the
backup publish details and asset UID mapping), with asset-scan gating applied.
Intended for the post-import publish flow.
--branch=<value> [default: main] The name of the branch where you want to perform the bulk publish
operation. If you don't mention the branch name, then by default the content from
main branch will be published.
Expand Down Expand Up @@ -114,7 +117,7 @@ EXAMPLES

$ csdx cm:stacks:bulk-assets --revert ./bulk-operation -a myAlias

$ csdx cm:stacks:bulk-assets --data-dir ./content --operation publish -k blt123
$ csdx cm:stacks:bulk-assets --backup-dir ./content --operation publish -k blt123

$ csdx cm:stacks:bulk-assets --operation delete --space-uid am123 --org-uid bltOrg --locale en-us --asset-uids-file ./assets.json

Expand Down
12 changes: 7 additions & 5 deletions packages/contentstack-bulk-operations/src/base-bulk-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ import {
setupStackConfig,
setupBatchQueueListeners,
confirmOperation as confirmOperationUtil,
getUniqueEnvironments,
getUniqueLocales,
batchItems,
hasPublishTargets,
handleCrossPublishOperation,
logOperationInfo,
validateBatch,
Expand Down Expand Up @@ -519,9 +518,12 @@ export abstract class BaseBulkCommand extends Command {
* Execute operation in BULK mode - processes items in batches
*/
private async executeBulkMode(items: any[], startTime: number): Promise<BulkOperationResult> {
const environments = getUniqueEnvironments(items);
const locales = getUniqueLocales(items);
const batches = batchItems(items, environments, locales);
const untargeted = items.filter((item) => !hasPublishTargets(item)).length;
if (untargeted > 0) {
this.logger.warn(`Skipping ${untargeted} item(s) with no publish target (missing publish_details).`);
}

const batches = batchItems(items);

batches.forEach((batch) => validateBatch(batch));
this.logger.debug(`Created ${batches.length} batches for processing`, this.loggerContext);
Expand Down
Loading
Loading