Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 6 additions & 6 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ fileignoreconfig:
- filename: src/core/failure.test.ts
checksum: 242d43885d1e3ef46edf1e347899a90549ebf95ec6827426746852f7a6dfc418
- filename: test/integration/retired-commands.test.ts
checksum: 3d2af18ef58effd89e398e6cb523f6680d7385eb34cc660f559be59114ab07f7
checksum: 620ae8b1cc10b2d1aed68418a0a82cfb0bb23edd99ad93e6f10ddf5089f446dd
- filename: test/integration/manage-base-path.test.ts
checksum: 79eb9ad420ea47d5bee8045d812c45b010bbeeafcaf5f85d11653140b0cd0db1
- filename: src/transport/rest-client.ts
checksum: bf0b19bdce7796208e7a8f29f7fdc8aad36ed2ffc10e148439fa19fd166c63d3
checksum: 755aabbc8ee7a2440d4b696e0c502b612c6a60c50fea38db6537d46849521b41
- filename: src/transport/rest-client.test.ts
checksum: 3b5751449c61f7a1d71b978ed03b9472f714e12dd1679f691bbd4dd8f84be923
checksum: 02981226eef6131634d0903fc04fa73a5fb8cac82213f00f49d5f66950d75f8e
- filename: src/transport/cma-client.ts
checksum: 87b8399a9b84ea60fe322802c2e5270772858ba85ac33c436941fe7cf40afc38
- filename: src/transport/auth-strategy.test.ts
Expand Down Expand Up @@ -228,7 +228,7 @@ fileignoreconfig:
- "a project name passed on argv to the project resolver"
- "expect\\(Object.keys\\(DEPENDENCIES\\)\\).toEqual\\(\\['project', 'env'\\]\\)"
- filename: AGENTS.md
checksum: dcebcddea3f29849c0b287e9cf8786a4e306394fe96b9ca4136525298eee6379
checksum: 2a47698f4c9e0f3576b0ccd5fd1852b144ee3729d4e1e38c828c0d77d042e3fd
allowed_patterns:
- "Do not add an exemption to a guard to make a new file pass"
- "passes its own error wording to `client.request\\(req, MESSAGES\\)`"
Expand All @@ -252,7 +252,7 @@ fileignoreconfig:
- filename: src/git/local-repository.ts
checksum: b9091ed135dfebdf590a5412bbfba2b2f96d4a177cf944a20909b93405268e9f
- filename: test/guards/layering.test.ts
checksum: 6740eea4e84f088081e31ac0c12c0abb2c402dcebbe0eb0ef454f60873da0194
checksum: f8d2a8cab01feca56c55bc17fb1bced4cf76575d87b19aaee9b73bab595e52a1
- filename: src/commands/launch/projects/create.test.ts
checksum: 9c40b4b6ed4f7d678ecb921a71f5af7284cd685c1337f47144456ae43a38b1ad
- filename: src/commands/launch/projects/delete.test.ts
Expand Down Expand Up @@ -324,7 +324,7 @@ fileignoreconfig:
- filename: src/commands/launch/env/update.test.ts
checksum: 2da219594e7bdd589adc0eff9ff035252a75aaa51dfa20514b18fd3266d7b752
- filename: README.md
checksum: b956e3b147ebb3513472f5f28f47f7895d86201d786f1ed80b10c1fd785267e7
checksum: 2e2ca75a02e98610c5fd8a3fa01a1def99b5ec161c80b2a76532fe039f051926
allowed_patterns:
- "password-protection"
- "-p, --password"
Expand Down
17 changes: 14 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ src/
git/ the internal git-namespace / repository / branch lookups
organizations/ the --org flag, its organization picker, and the lookup behind it,
which reads the Contentstack Management API rather than Launch
cache/ the CDN cache purge behind launch:cache:purge: CacheApi, the --path / --tags / --hosts
inputs and their normalisation, and the success lines
functions/ the cloud-function runtime and the serve flags, treated as a resource
resources.ts the composition root: it assembles the catalog, the resolution table,
the dependency map and the api surface out of the resources
Expand Down Expand Up @@ -552,6 +554,16 @@ mutation, because the server may already have acted on it. A `Retry-After` given
lengthens the wait up to `MAX_RETRY_AFTER_MS` (30 s); an HTTP date or anything unparseable falls back
to the policy's own backoff.

A request can opt out of retrying a 429 by code: `RestRequest.noRetryCodes` lists the error codes whose 429
is final, and `RetryPolicy.shouldRetry` takes the codes the answer carried and refuses to retry when any is
listed. It is per request, so a 429 with any other code, or with no code, and every request that sets nothing,
still retries on the usual budget. Only `CacheApi.purge` sets it, with
`launch.ORGANIZATION.REVALIDATE_RATE_LIMIT_EXCEEDED`: that quota is a fixed UTC day already spent, so a retry
only delays the error and can never succeed. `test/integration/transport-retry.test.ts` proves on the wire that the
quota 429 is sent once and an ordinary 429 is sent `maxRetries + 1` times. `launch:cache:purge` rewords a 403 from
the purge whatever its code (`asPurgeFailure`), and a 404 from its environment read as the usage error `No
environment with UID "<uid>" found in this project.` (exit 2), so a foreign environment is refused before any POST.

A proxy URL shown in a diagnostic has its `user:…@` part removed first (`withoutCredentials`), and
`https_proxy`/`http_proxy` are read as well as the uppercase names.

Expand Down Expand Up @@ -821,7 +833,7 @@ checklist and nothing else; the create summary says `ready to set` because the e
oclif's native `exclusive` / `relationships` on the flag definition, where it also shows in `--help` -
and a simple range does too: `limit` and `skip` carry oclif's own `min`/`max` rather than being
checked later. A rule that must read a *resolved* value (one that config, a prompt or a default may
have supplied) belongs in `src/core/rules.ts` - there are three: `atLeastOneOf` and `exactlyOneOf` (no consumer yet;
have supplied) belongs in `src/core/rules.ts` - there are three: `exactlyOneOf` (used by `cache:purge`), `atLeastOneOf` (no consumer yet;
see "Built ahead of use" below) and `onlyWithValueOf` (used by `projects:create`) - declared as a `static rules = [...]` array on the command. `resolveInputs` evaluates them after resolution,
and a failing rule is a usage error (exit 2).

Expand All @@ -834,15 +846,14 @@ gate value `onlyWithValueOf` reads is a value, not a "was it supplied?" question
wherever it came from. Write the next rule when a command needs it; a rule
kept alive only by its own test proves nothing.

**Built ahead of use.** Four pieces of production code have no caller yet, on purpose, each held
**Built ahead of use.** Three pieces of production code have no caller yet, on purpose, each held
for a named ticket whose shape it already fits. Nothing else in `src` is uncalled; add to this list
rather than leave an orphan unexplained.

| Code | Held for | Why its API fits |
|---|---|---|
| `DeploymentsApi.list` | CL-7170 (`deployments:*`) | a `DeploymentScope` plus paging -> `DeploymentsPage` |
| `src/core/redact.ts` | CL-7169 (variables) | see below |
| `exactlyOneOf` | CL-7172 (`cache:purge`) | "purge these paths, or everything, not both" - and it judges what the user supplied, not defaults |
| `atLeastOneOf` | no ticket yet - its caller, `projects:update`, now prompts for missing fields instead | "at least one of these flags" for the next command that must not prompt - and it judges what the user supplied, not defaults |

**Redaction.** `src/core/redact.ts` (`REDACTED`, `redactedColumn`) has no caller yet; it is kept
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ with exit code 2 and name their V2 replacements. `launch:functions:serve` is liv
<!-- commands -->
* [`csdx help [COMMAND]`](#csdx-help-command)
* [`csdx launch`](#csdx-launch)
* [`csdx launch:cache:purge`](#csdx-launchcachepurge)
* [`csdx launch:deployments`](#csdx-launchdeployments)
* [`csdx launch:env:create`](#csdx-launchenvcreate)
* [`csdx launch:env:delete`](#csdx-launchenvdelete)
Expand Down Expand Up @@ -99,6 +100,41 @@ DESCRIPTION

_See code: [src/commands/launch/index.ts](https://github.com/contentstack/launch-cli/blob/v2.0.0-beta.0/src/commands/launch/index.ts)_

## `csdx launch:cache:purge`

Purge an environment's CDN cache by path, cache tags or hostnames

```
USAGE
$ csdx launch:cache:purge [-c <value>] [-d <value>] [--org <value>] [--project <value>] [--env <value>] [--path
<value>] [--tags <value>] [--hosts <value>]

FLAGS
-c, --config=<value> Path to the local '.cs-launch.json' file
-d, --data-dir=<value> Current working directory
--env=<value> Environment name or UID
--hosts=<value> Comma-separated hostnames to purge
--org=<value> Organization UID
--path=<value> Path to purge, for example /blog. End it with /* to purge everything beneath it, or pass /*
for the whole site
--project=<value> Project name or UID
--tags=<value> Comma-separated cache tags to purge

DESCRIPTION
Purge an environment's CDN cache by path, cache tags or hostnames

EXAMPLES
$ csdx launch:cache:purge --org <org-uid> --project <name-or-uid> --env <name-or-uid> --path /blog

$ csdx launch:cache:purge --org <org-uid> --project <name-or-uid> --env <name-or-uid> --path "/blog/*"

$ csdx launch:cache:purge --org <org-uid> --project <name-or-uid> --env <name-or-uid> --tags header,footer

$ csdx launch:cache:purge --org <org-uid> --project <name-or-uid> --env <name-or-uid> --hosts www.example.com
```

_See code: [src/commands/launch/cache/purge.ts](https://github.com/contentstack/launch-cli/blob/v2.0.0-beta.0/src/commands/launch/cache/purge.ts)_

## `csdx launch:deployments`

Removed in v2 - list deployments with launch:deployments:list
Expand Down
22 changes: 13 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"adm-zip": "^0.6.1",
"dotenv": "^16.4.7",
"express": "^4.22.0",
"express": "^4.22.3",
"https-proxy-agent": "^5.0.1",
"open": "^8.4.2",
"rollup": "^4.63.4",
Expand Down Expand Up @@ -73,6 +73,9 @@
"launch:env": {
"description": "Manage the environments of a Launch project"
},
"launch:cache": {
"description": "Purge the CDN cache of a Launch environment"
},
"launch:variables": {
"description": "List, set and remove the environment variables of a Launch environment"
},
Expand Down Expand Up @@ -145,6 +148,7 @@
"csdxConfig": {
"shortCommandName": {
"launch": "LNCH",
"launch:cache:purge": "LNCH-CCH-PRG",
"launch:deployments": "LNCH-DPLMNT",
"launch:environments": "LNCH-ENV",
"launch:env:create": "LNCH-ENV-CRT",
Expand Down
74 changes: 74 additions & 0 deletions src/cache/cache.api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { RestApiClient, RestRequest } from '../transport/rest-client';
import { LaunchApiError } from '../transport/errors';
import { CacheApi } from './cache.api';
import { CACHE_ERROR_MESSAGES } from './cache.errors';

const QUOTA_CODE = 'launch.ORGANIZATION.REVALIDATE_RATE_LIMIT_EXCEEDED';

function fakeRestClient(failure?: Error) {
const requests: RestRequest[] = [];
const messages: unknown[] = [];
const client = {
request: async (req: RestRequest, errorMessages: unknown) => {
requests.push(req);
messages.push(errorMessages);

if (failure) {
throw failure;
}

return { message: 'The CDN cache revalidation task has been initiated.' };
},
} as unknown as RestApiClient;

return { client, requests, messages };
}

function purgeTags(client: RestApiClient): Promise<void> {
return new CacheApi(client).purge({ org: 'o', project: 'p', environment: 'e', criterion: { cacheTags: ['a'] } });
}

describe('CacheApi', () => {
it('posts the criterion to the environment-scoped revalidate-cdn-cache route with the cache wording', async () => {
const { client, requests, messages } = fakeRestClient();

const result = await new CacheApi(client).purge({
org: 'org1',
project: 'proj1',
environment: 'env1',
criterion: { cacheTags: ['a'] },
});

expect(result).toBeUndefined();
expect(requests).toEqual([
{
method: 'POST',
path: '/projects/proj1/environments/env1/revalidate-cdn-cache',
orgUid: 'org1',
projectUid: 'proj1',
body: { cacheTags: ['a'] },
noRetryCodes: [QUOTA_CODE],
},
]);
expect(messages[0]).toBe(CACHE_ERROR_MESSAGES);
});

it('propagates a failed request rather than reporting a purge', async () => {
const failure = new Error('purge exploded');
const { client } = fakeRestClient(failure);

await expect(
new CacheApi(client).purge({ org: 'o', project: 'p', environment: 'e', criterion: { hostnames: ['x'] } }),
).rejects.toBe(failure);
});

it('rewords a 403 whatever its code as needing permission to update the environment', async () => {
const { client } = fakeRestClient(new LaunchApiError(403, [{ code: 'launch.FORBIDDEN' }]));

const error = await purgeTags(client).catch((caught: unknown) => caught);

expect(error).toBeInstanceOf(LaunchApiError);
expect(error).toMatchObject({ status: 403, exitCode: 1 });
expect((error as Error).message).toBe('Purging the CDN cache needs permission to update the environment.');
});
});
27 changes: 27 additions & 0 deletions src/cache/cache.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { RestApiClient } from '../transport/rest-client';
import { CACHE_ERROR_MESSAGES, PURGE_QUOTA_CODE, asPurgeFailure } from './cache.errors';
import type { PurgeCacheParams } from './types';

export * from './types';

export class CacheApi {
constructor(private readonly client: RestApiClient) {}

async purge({ org, project, environment, criterion }: PurgeCacheParams): Promise<void> {
try {
await this.client.request<unknown>(
{
method: 'POST',
path: `/projects/${project}/environments/${environment}/revalidate-cdn-cache`,
orgUid: org,
projectUid: project,
body: criterion,
noRetryCodes: [PURGE_QUOTA_CODE],
},
CACHE_ERROR_MESSAGES,
);
} catch (error) {
throw asPurgeFailure(error);
}
}
}
Loading
Loading