Skip to content

Commit c6f4afb

Browse files
Merge pull request #261 from contentstack/development
Update deployment logs query to V2, Add rollback plan-not-available message and Upgrade cli-utilities with Snyk fixes
2 parents a2df5fd + 11fb180 commit c6f4afb

40 files changed

Lines changed: 4127 additions & 422 deletions

‎.github/workflows/secrets-scan.yml‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Secrets Scan
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
secrets-scan:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
16+
with:
17+
ref: ${{ github.event.pull_request.head.sha }}
18+
fetch-depth: 0
19+
20+
- name: Run Talisman
21+
env:
22+
TALISMAN_VERSION: v1.37.0
23+
# Pinned from https://github.com/thoughtworks/talisman/releases/download/v1.37.0/checksums -
24+
# committed here rather than fetched at runtime, since fetching the checksums file from the
25+
# same release as the binary it's meant to verify wouldn't catch a compromised release.
26+
TALISMAN_SHA256: 8e0ae8bb7b160bf10c4fa1448beb04a32a35e63505b3dddff74a092bccaaa7e4
27+
run: |
28+
set -euo pipefail
29+
curl -fsSL -o talisman_linux_amd64 "https://github.com/thoughtworks/talisman/releases/download/${TALISMAN_VERSION}/talisman_linux_amd64"
30+
echo "${TALISMAN_SHA256} talisman_linux_amd64" | sha256sum -c -
31+
chmod +x talisman_linux_amd64
32+
echo "refs/heads/${{ github.head_ref }} ${{ github.event.pull_request.head.sha }} refs/heads/${{ github.base_ref }} ${{ github.event.pull_request.base.sha }}" | ./talisman_linux_amd64 --githook pre-push
33+
34+
- name: TruffleHog OSS
35+
uses: trufflesecurity/trufflehog@6f3c981e7b77f235fd2702dd74af25fc4b72bf11 # v3.96.0
36+
with:
37+
path: ./
38+
base: ${{ github.event.pull_request.base.sha }}
39+
head: ${{ github.event.pull_request.head.sha }}
40+
extra_args: --results=verified,unverified,unknown

‎.husky/pre-commit‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
npm run prepack && npm run lint-staged
2-
3-
echo "\n\n Running Talisman secret scan..."
4-
talisman --githook pre-commit
5-
6-
echo "\n\n Running Snyk vulnerability scan..."
1+
npm run prepack && npm run lint-staged && \
2+
echo "\n\n Running secrets scan (Talisman + trufflehog)..." && \
3+
sh .husky/secrets-scan.sh pre-commit && \
4+
echo "\n\n Running Snyk vulnerability scan..." && \
75
snyk test --all-projects --fail-on=all

‎.husky/pre-push‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
npm run test
1+
sh .husky/secrets-scan.sh pre-push "$@" && npm run test

‎.husky/secrets-scan.sh‎

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
cd "$(git rev-parse --show-toplevel)"
5+
6+
for tool in git curl tar mktemp awk xargs uname tr dirname; do
7+
command -v "$tool" >/dev/null 2>&1 || { echo "secrets-scan: required tool '$tool' not found in PATH" >&2; exit 1; }
8+
done
9+
if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1; then
10+
echo "secrets-scan: required tool 'sha256sum' or 'shasum' not found in PATH" >&2
11+
exit 1
12+
fi
13+
14+
HOOK_NAME="$1"
15+
16+
TALISMAN_VERSION="v1.37.0"
17+
TRUFFLEHOG_VERSION="3.96.0"
18+
CACHE_DIR="$HOME/.cache/secrets-scan-tools"
19+
mkdir -p "$CACHE_DIR"
20+
21+
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
22+
ARCH_RAW=$(uname -m)
23+
case "$ARCH_RAW" in
24+
x86_64) ARCH=amd64 ;;
25+
arm64|aarch64) ARCH=arm64 ;;
26+
*) ARCH="$ARCH_RAW" ;;
27+
esac
28+
29+
# Pinned checksums (from https://github.com/thoughtworks/talisman/releases/download/v1.37.0/checksums
30+
# and https://github.com/trufflesecurity/trufflehog/releases/download/v3.96.0/trufflehog_3.96.0_checksums.txt),
31+
# committed here rather than fetched at runtime - the whole point is to not trust a checksums file
32+
# downloaded from the same release as the binary it's meant to verify.
33+
case "${OS}_${ARCH}" in
34+
darwin_amd64)
35+
TALISMAN_SHA256="9a67e015d6c99650c45d653ce4a597f885bf661164c2671f08a3827013b277fb"
36+
TRUFFLEHOG_SHA256="a30d8f1095e031a81a668e1582f2ed479c3b50476cef86317e0fb74210c33617"
37+
;;
38+
darwin_arm64)
39+
TALISMAN_SHA256="4806746b2ba0190941f0606853990b4da6eb7df2d64c261212a6aa66eb6b15c2"
40+
TRUFFLEHOG_SHA256="87478306b95ca2420cfb844b7582383ac60b922e262350a0088e797f328d2e62"
41+
;;
42+
linux_amd64)
43+
TALISMAN_SHA256="8e0ae8bb7b160bf10c4fa1448beb04a32a35e63505b3dddff74a092bccaaa7e4"
44+
TRUFFLEHOG_SHA256="7105f1cd6577f058a9e39d0578f1a99c8a1e481e4d3512cd8a09acfe22a0fdc0"
45+
;;
46+
linux_arm64)
47+
TALISMAN_SHA256="ca706f428a45d7a4ed3cc5c3cf7b3e5b0dfce6f2f584000a117d0d602a0342b7"
48+
TRUFFLEHOG_SHA256="50acd4c7a3b8ebfe5083d8350956057030c44be3515dedd55b45263495c490b2"
49+
;;
50+
*)
51+
echo "secrets-scan: no pinned checksum for platform ${OS}_${ARCH} (talisman ${TALISMAN_VERSION} / trufflehog ${TRUFFLEHOG_VERSION})" >&2
52+
exit 1
53+
;;
54+
esac
55+
56+
if command -v sha256sum >/dev/null 2>&1; then
57+
checksum() { sha256sum "$1" | awk '{print $1}'; }
58+
else
59+
checksum() { shasum -a 256 "$1" | awk '{print $1}'; }
60+
fi
61+
62+
TALISMAN_BIN="$CACHE_DIR/talisman_${TALISMAN_VERSION}_${OS}_${ARCH}"
63+
if [ ! -x "$TALISMAN_BIN" ]; then
64+
echo "secrets-scan: downloading Talisman ${TALISMAN_VERSION}..."
65+
curl -fsSL -o "${TALISMAN_BIN}.tmp" "https://github.com/thoughtworks/talisman/releases/download/${TALISMAN_VERSION}/talisman_${OS}_${ARCH}"
66+
ACTUAL_HASH=$(checksum "${TALISMAN_BIN}.tmp")
67+
if [ "$ACTUAL_HASH" != "$TALISMAN_SHA256" ]; then
68+
echo "secrets-scan: Talisman checksum verification FAILED" >&2
69+
rm -f "${TALISMAN_BIN}.tmp"
70+
exit 1
71+
fi
72+
mv "${TALISMAN_BIN}.tmp" "$TALISMAN_BIN"
73+
chmod +x "$TALISMAN_BIN"
74+
fi
75+
76+
TRUFFLEHOG_DIR="$CACHE_DIR/trufflehog_${TRUFFLEHOG_VERSION}_${OS}_${ARCH}"
77+
TRUFFLEHOG_BIN="$TRUFFLEHOG_DIR/trufflehog"
78+
if [ ! -x "$TRUFFLEHOG_BIN" ]; then
79+
echo "secrets-scan: downloading trufflehog ${TRUFFLEHOG_VERSION}..."
80+
mkdir -p "$TRUFFLEHOG_DIR"
81+
TARBALL="$CACHE_DIR/trufflehog_${TRUFFLEHOG_VERSION}_${OS}_${ARCH}.tar.gz"
82+
curl -fsSL -o "$TARBALL" "https://github.com/trufflesecurity/trufflehog/releases/download/v${TRUFFLEHOG_VERSION}/trufflehog_${TRUFFLEHOG_VERSION}_${OS}_${ARCH}.tar.gz"
83+
ACTUAL_HASH=$(checksum "$TARBALL")
84+
if [ "$ACTUAL_HASH" != "$TRUFFLEHOG_SHA256" ]; then
85+
echo "secrets-scan: trufflehog checksum verification FAILED" >&2
86+
rm -f "$TARBALL"
87+
exit 1
88+
fi
89+
tar -xzf "$TARBALL" -C "$TRUFFLEHOG_DIR"
90+
chmod +x "$TRUFFLEHOG_BIN"
91+
fi
92+
93+
case "$HOOK_NAME" in
94+
pre-commit)
95+
"$TALISMAN_BIN" --githook pre-commit
96+
97+
STAGED_LIST=$(mktemp)
98+
STAGE_DIR=$(mktemp -d)
99+
trap 'rm -f "$STAGED_LIST"; rm -rf "$STAGE_DIR"' EXIT
100+
git diff --cached --name-only --diff-filter=ACM -z > "$STAGED_LIST"
101+
if [ -s "$STAGED_LIST" ]; then
102+
export STAGE_DIR
103+
xargs -0 sh -c 'for f; do mkdir -p "$STAGE_DIR/$(dirname "$f")"; git show ":$f" > "$STAGE_DIR/$f"; done' _ < "$STAGED_LIST"
104+
"$TRUFFLEHOG_BIN" filesystem --fail --no-update --results=verified,unverified,unknown "$STAGE_DIR"
105+
fi
106+
;;
107+
pre-push)
108+
REMOTE_NAME="${2:-origin}"
109+
STDIN_DATA=$(cat)
110+
if [ -z "$STDIN_DATA" ]; then
111+
exit 0
112+
fi
113+
114+
DEFAULT_REF=$(git symbolic-ref -q --short "refs/remotes/${REMOTE_NAME}/HEAD" 2>/dev/null || echo "")
115+
DEFAULT_BASE=""
116+
[ -n "$DEFAULT_REF" ] && DEFAULT_BASE=$(git rev-parse --verify "$DEFAULT_REF" 2>/dev/null || echo "")
117+
[ -z "$DEFAULT_BASE" ] && DEFAULT_BASE=$(git rev-parse --verify "${REMOTE_NAME}/main" 2>/dev/null || echo "")
118+
[ -z "$DEFAULT_BASE" ] && DEFAULT_BASE=$(git rev-parse --verify "${REMOTE_NAME}/master" 2>/dev/null || echo "")
119+
120+
RESOLVED_STDIN=""
121+
while read -r LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do
122+
[ -z "$LOCAL_SHA" ] && continue
123+
[ "$LOCAL_SHA" = "0000000000000000000000000000000000000000" ] && continue
124+
if [ -z "$REMOTE_SHA" ] || [ "$REMOTE_SHA" = "0000000000000000000000000000000000000000" ]; then
125+
REMOTE_SHA="$DEFAULT_BASE"
126+
fi
127+
if [ -z "$REMOTE_SHA" ]; then
128+
echo "secrets-scan: could not determine a base commit to scan ${LOCAL_REF} against (no ${REMOTE_NAME}/HEAD, ${REMOTE_NAME}/main, or ${REMOTE_NAME}/master found locally) - refusing to silently skip the scan. Run 'git fetch ${REMOTE_NAME}' and try again." >&2
129+
exit 1
130+
fi
131+
RESOLVED_STDIN="${RESOLVED_STDIN}${LOCAL_REF} ${LOCAL_SHA} ${REMOTE_REF} ${REMOTE_SHA}
132+
"
133+
"$TRUFFLEHOG_BIN" git "file://$(pwd)" --since-commit "$REMOTE_SHA" --branch "$LOCAL_SHA" --fail --no-update --results=verified,unverified,unknown
134+
done <<EOF
135+
$STDIN_DATA
136+
EOF
137+
138+
if [ -n "$RESOLVED_STDIN" ]; then
139+
printf '%s' "$RESOLVED_STDIN" | "$TALISMAN_BIN" --githook pre-push
140+
fi
141+
;;
142+
*)
143+
echo "secrets-scan: unknown hook '$HOOK_NAME'" >&2
144+
exit 1
145+
;;
146+
esac

‎.talismanrc‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,44 @@ fileignoreconfig:
22
- filename: .github/workflows/secrets-scan.yml
33
ignore_detectors:
44
- filecontent
5+
- filename: .husky/secrets-scan.sh
6+
checksum: 5a44edca8d209ca4b7f6eefc4ce2caab838679f8916ef6136ad68584c0705cb6
57
- filename: src/commands/launch/index.test.ts
68
checksum: 9db6c02ad35a0367343cd753b916dd64db4a9efd24838201d2e1113ed19c9b62
79
- filename: src/commands/launch/rollback.test.ts
810
checksum: a1010882456f315a918afe2777f90472985e9966bd308c5311ac0de318b14e8c
911
- filename: test/unit/commands/rollback.test.ts
1012
checksum: d1f931f2d9a397131409399ad6463653e28b5a2224e870b641d9ba57c4418f18
1113
- filename: package-lock.json
12-
checksum: a713321dbdd291f8087846c9c45793588787eb8adc76faf4f74968964e502d3e
14+
ignore_detectors:
15+
- filecontent
1316
- filename: src/adapters/github.test.ts
1417
checksum: b3d3d3a3c14adde103152d2ac4e1c4b7121a5f7533a87c3cc600f6d25fb59d1b
1518
- filename: src/adapters/file-upload.test.ts
16-
checksum: 962fd8b45578914926d20857ddc3d1bc2fa19e8b192c6dfdc03d214ae1898b59
17-
version: "1.0"
19+
allowed_patterns:
20+
- "--(disable|enable)-cs-auth is passed"
21+
- filename: src/adapters/file-upload.ts
22+
allowed_patterns:
23+
- "const \\{ formFieldKey, formFieldValue \\} of fields"
24+
- "formData\\.append\\(formFieldKey, formFieldValue\\)"
25+
- "headers: \\{ key: string; value: string \\}\\[\\]"
26+
- "headers\\?\\.reduce\\(\\(acc, \\{ key, value \\}\\)"
27+
- "\\(\\{ key, value \\}\\) => \\(\\{ key, value \\}\\)\\)"
28+
- "const \\{ token, apiKey \\} = configHandler\\.get"
29+
- filename: test/integration/specs/environments.test.ts
30+
checksum: f02e76dc6f4dfac2bc21d53357d70fc2b93c6473b9e71ab08300670f2c5649c1
31+
- filename: test/integration/jest.setup-env.ts
32+
checksum: e82558593f4614fc62127f01ea8beb95231c95826b65071eb3971f9c45332344
33+
- filename: test/integration/harness/launch-api.ts
34+
checksum: a4549b87c896744422e71ab5395a1d6d2dfa372678d2488aecc4247ee9738f87
35+
- filename: test/integration/harness/session.ts
36+
checksum: 74dc637304710b1df882bea6a1c52b8779f1bd416dbeeab93a9026c68edb3f9e
37+
- filename: test/integration/specs/launch-github.test.ts
38+
checksum: 4ea1ae16627383530d7590374ba2b37fde87b5c111cc6c38c5125456aa9b51c8
39+
- filename: test/integration/harness/fixtures.ts
40+
checksum: b3133a65f3f4b3f3a32caf0cc0bf65b0c71187f96e044a3331ddcce3adea88c1
41+
- filename: src/base-command.ts
42+
checksum: db8bc45f6d8aa3daebae621920c8618b6e23d1a20984dee0749d91bfa5c1530c
43+
- filename: test/unit/base-command.test.ts
44+
checksum: 4612c2a41c9d5a7af887f77842feee0a6f6a28ba95a094282b5e9aa20dba6c8c
45+
version: "1.0"

‎AGENTS.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,33 @@
88
- Use only jest for writing test cases and refer existing unit test under the /src folder.
99
- Do not create code comments for any changes.
1010

11+
## Integration tests
12+
13+
- Unit tests mock the CLI's own classes, so they keep passing when behaviour changes.
14+
Integration tests (`test/integration/`) exist to catch what unit tests cannot: they run a
15+
real oclif command end to end and assert on what the CLI *does*.
16+
- Run them with `npm run test:integration` (they are also part of `npm test`).
17+
- Only three things are replaced: the terminal prompt (`inquirer`), the `open` package, and
18+
the Contentstack management SDK, which resolves its base URL as `https://<host>:443` and so
19+
cannot be pointed at a local server. Apollo, the GraphQL documents, the CLI config store,
20+
flag parsing, the filesystem and `git` all run for real.
21+
- The Launch API is a local HTTP server (`test/integration/harness/launch-api.ts`) that the CLI
22+
reaches through a temporary region config. It records every GraphQL operation, so tests assert
23+
on the operations and variables the CLI actually sent rather than on internal method calls.
24+
- Assert on the observable contract - operation sequence, request variables, prompt sequence,
25+
`.cs-launch.json` contents, stdout, exit code. Never stub a method on a command or adapter;
26+
that is what makes these tests survive a refactor and fail on a behaviour change.
27+
- Register a response for every operation a flow needs and finish with
28+
`session.api.assertNoUnhandledOperations()` and `prompts.assertScriptFullyConsumed()`, so a
29+
renamed operation or an added prompt fails loudly instead of passing quietly.
30+
- V2 (epic CL-4867) removes the bare `csdx launch` command in favour of `projects:create`,
31+
`environments:create` and `deployments:create`. `launch-github.test.ts` and
32+
`launch-file-upload.test.ts` therefore describe behaviour that has to be *ported* to those
33+
commands, not a command that survives — read a failure there as "the new command does something
34+
different", and repoint the spec rather than deleting the assertion.
35+
- Tests named after a `CL-` ticket pin a bug that was already fixed once. Do not relax them.
36+
- `forceExit: true` in the integration jest config papers over a known-benign open handle:
37+
oclif's `Config.load()` eagerly loads every command manifest, so `launch:functions` pulls in
38+
rollup's native bindings and their GC handle. The cost is that a *future* spec which genuinely
39+
hangs will force-exit instead of failing loudly — run `--detectOpenHandles` periodically to
40+
check nothing real is hiding behind it.

‎eslint.config.mjs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ export default tseslint.config(
4040
// test files use @ts-nocheck and scaffolding vars that aren't all referenced
4141
'@typescript-eslint/ban-ts-comment': 'off',
4242
'@typescript-eslint/no-unused-vars': 'off',
43+
// harness mirrors untyped CLI payloads, and jest.mock factories must use require()
44+
'@typescript-eslint/no-explicit-any': 'off',
45+
'@typescript-eslint/no-require-imports': 'off',
46+
},
47+
},
48+
{
49+
files: ['*.config.js'],
50+
languageOptions: {
51+
sourceType: 'commonjs',
52+
globals: {
53+
module: 'writable',
54+
require: 'readonly',
55+
process: 'readonly',
56+
__dirname: 'readonly',
57+
},
4358
},
4459
},
4560
);

‎jest.integration.config.js‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const config = {
2+
testEnvironment: 'node',
3+
rootDir: '.',
4+
testMatch: ['<rootDir>/test/integration/specs/**/*.test.ts'],
5+
transform: {
6+
'^.+\\.ts$': ['ts-jest', { tsconfig: '<rootDir>/test/integration/tsconfig.json' }],
7+
'^.+\\.js$': [
8+
'ts-jest',
9+
{ tsconfig: { allowJs: true, module: 'commonjs', target: 'es2020', esModuleInterop: true } },
10+
],
11+
},
12+
transformIgnorePatterns: ['/node_modules/(?!uuid/)'],
13+
moduleNameMapper: { '^chalk$': require.resolve('chalk') },
14+
globalSetup: '<rootDir>/test/integration/jest.global-setup.ts',
15+
globalTeardown: '<rootDir>/test/integration/jest.global-teardown.ts',
16+
setupFiles: ['<rootDir>/test/integration/jest.setup-env.ts'],
17+
setupFilesAfterEnv: ['<rootDir>/test/integration/jest.setup.ts'],
18+
maxWorkers: 1,
19+
testTimeout: 30000,
20+
forceExit: true,
21+
coverageDirectory: 'coverage/integration',
22+
coverageProvider: 'v8',
23+
collectCoverageFrom: ['src/**/*.ts', '!src/test/**', '!src/**/*.test.ts'],
24+
};
25+
26+
module.exports = config;

0 commit comments

Comments
 (0)