@nullplatform/mcp 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/i18n.js +2 -0
- package/dist/np/context.js +39 -28
- package/dist/np/journey.js +15 -6
- package/dist/surfaces/developer.js +3 -1
- package/dist/tools/builds.js +11 -4
- package/dist/tools/create-release.js +1 -1
- package/dist/tools/deploy.js +1 -1
- package/dist/tools/find-apps.js +1 -1
- package/dist/tools/logs.js +1 -1
- package/dist/tools/params.js +1 -1
- package/dist/tools/playbook.js +2 -2
- package/dist/tools/status.js +2 -2
- package/dist/tools/traffic.js +2 -2
- package/package.json +1 -1
- package/skills/configuring-safely/SKILL.md +50 -0
- package/skills/deploying-safely/SKILL.md +6 -3
- package/skills/incident-response/SKILL.md +4 -1
- package/skills/managing-environments/SKILL.md +53 -0
- package/skills/platform-conventions/SKILL.md +19 -9
- package/skills/promoting-across-environments/SKILL.md +51 -0
- package/skills/tracing-a-change/SKILL.md +30 -0
- package/skills/understand-a-service/SKILL.md +50 -0
- package/skills/working-from-a-ticket/SKILL.md +47 -0
- package/widgets-dist/create-app.html +2 -2
- package/widgets-dist/find-apps.html +26 -26
- package/widgets-dist/logs.html +2 -2
- package/widgets-dist/manifest.json +6 -6
- package/widgets-dist/metrics.html +2 -2
- package/widgets-dist/np-panel.html +2 -2
- package/widgets-dist/params.html +11 -11
package/dist/i18n.js
CHANGED
|
@@ -160,6 +160,7 @@ const english = {
|
|
|
160
160
|
"builds.commit": "Commit",
|
|
161
161
|
"builds.released": "Released",
|
|
162
162
|
"builds.none": "**{app}** has no builds yet.",
|
|
163
|
+
"builds.noneForCommit": "No build in **{app}** for commit `{commit}` — it may not have built yet, or the commit isn't on a built branch.",
|
|
163
164
|
"builds.noneHint": "push a commit so CI produces one, then `application_deployment_create`.",
|
|
164
165
|
"builds.deployHint": "`application_deployment_create build_id:{build}` ships it (cuts the release for you).",
|
|
165
166
|
"builds.waiting": "build #{build} is still running — `application_build_list` again in a moment to check.",
|
|
@@ -423,6 +424,7 @@ const spanish = {
|
|
|
423
424
|
"builds.commit": "Commit",
|
|
424
425
|
"builds.released": "Released",
|
|
425
426
|
"builds.none": "**{app}** todavía no tiene builds.",
|
|
427
|
+
"builds.noneForCommit": "Ningún build en **{app}** para el commit `{commit}` — puede que todavía no haya buildeado, o que el commit no esté en una branch que buildea.",
|
|
426
428
|
"builds.noneHint": "pusheá un commit para que CI genere uno, después `application_deployment_create`.",
|
|
427
429
|
"builds.deployHint": "`application_deployment_create build_id:{build}` lo publica (crea la release por vos).",
|
|
428
430
|
"builds.waiting": "el build #{build} sigue corriendo — `application_build_list` de nuevo en un momento para chequear.",
|
package/dist/np/context.js
CHANGED
|
@@ -12,10 +12,15 @@ export function baseRepoUrl(provider, prefix) {
|
|
|
12
12
|
: "https://github.com";
|
|
13
13
|
return `${host}/${prefix}`;
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
15
|
+
/** Org-wide app discovery must fan out across namespaces: the public `/application` endpoint is
|
|
16
|
+
* namespace-scoped, with no org-wide list and no name search — so the name filter is client-side
|
|
17
|
+
* by necessity. We page each namespace to completion and bound the AGGREGATE at this cap, which
|
|
18
|
+
* is generous and lift-able via NP_MAX_APPS, reporting truncation past it instead of silently
|
|
19
|
+
* hiding a large org (the old code capped the whole org at 200 with a flat `.slice`). */
|
|
20
|
+
const MAX_APPS = Math.max(1, Number(process.env.NP_MAX_APPS) || 1000);
|
|
21
|
+
export const DEFAULT_APP_LIMIT = MAX_APPS;
|
|
22
|
+
/** Safety ceiling when paging the account/namespace skeleton (both rarely exceed one page). */
|
|
23
|
+
const SKELETON_CAP = 2000;
|
|
19
24
|
/** Concurrency-limited parallel map — fast fan-out without hammering the API. */
|
|
20
25
|
export async function pmap(items, mapItem, limit = 12) {
|
|
21
26
|
const results = new Array(items.length);
|
|
@@ -29,6 +34,20 @@ export async function pmap(items, mapItem, limit = 12) {
|
|
|
29
34
|
await Promise.all(Array.from({ length: Math.min(limit, items.length || 1) }, worker));
|
|
30
35
|
return results;
|
|
31
36
|
}
|
|
37
|
+
/** Page an offset-based list endpoint to completion, bounded by `max` items. No list response
|
|
38
|
+
* carries a total, so the boundary is the standard offset heuristic: a page shorter than the
|
|
39
|
+
* requested limit is the last one. */
|
|
40
|
+
async function fetchPaged(fetchPage, max, pageSize = 200) {
|
|
41
|
+
const rows = [];
|
|
42
|
+
for (let offset = 0; offset < max; offset += pageSize) {
|
|
43
|
+
const limit = Math.min(pageSize, max - offset);
|
|
44
|
+
const page = await fetchPage(offset, limit);
|
|
45
|
+
rows.push(...page);
|
|
46
|
+
if (page.length < limit)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
32
51
|
/**
|
|
33
52
|
* The public list endpoints are NRN-authorized and need explicit scoping (account needs
|
|
34
53
|
* organization_id, namespace needs account_id, application needs namespace_id). There is no
|
|
@@ -53,16 +72,17 @@ export class NpContext {
|
|
|
53
72
|
if (!refresh && this.skeleton && Date.now() - this.skeleton.at < NpContext.CACHE_TTL_MS)
|
|
54
73
|
return this.skeleton;
|
|
55
74
|
const orgId = await this.organizationId();
|
|
56
|
-
const
|
|
57
|
-
organization_id: orgId,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const perAccount = await pmap(
|
|
61
|
-
const
|
|
62
|
-
.get("/namespace", { account_id: account.id, limit
|
|
63
|
-
.
|
|
75
|
+
const accounts = await fetchPaged((offset, limit) => this.np
|
|
76
|
+
.get("/account", { organization_id: orgId, offset, limit })
|
|
77
|
+
.then((page) => page.results ?? [])
|
|
78
|
+
.catch(() => []), SKELETON_CAP);
|
|
79
|
+
const perAccount = await pmap(accounts, async (account) => {
|
|
80
|
+
const accountNamespaces = await fetchPaged((offset, limit) => this.np
|
|
81
|
+
.get("/namespace", { account_id: account.id, offset, limit })
|
|
82
|
+
.then((page) => page.results ?? [])
|
|
83
|
+
.catch(() => []), SKELETON_CAP);
|
|
64
84
|
const accountBaseRepoUrl = baseRepoUrl(account.repository_provider, account.repository_prefix);
|
|
65
|
-
return
|
|
85
|
+
return accountNamespaces.map((namespace) => ({
|
|
66
86
|
id: namespace.id,
|
|
67
87
|
name: namespace.name,
|
|
68
88
|
nrn: namespace.nrn,
|
|
@@ -109,21 +129,12 @@ export class NpContext {
|
|
|
109
129
|
? skeleton.namespaces.filter((namespace) => namespace.name.toLowerCase().includes(namespaceFilter))
|
|
110
130
|
: skeleton.namespaces;
|
|
111
131
|
const nameFilter = args.query?.toLowerCase();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (args.query)
|
|
119
|
-
query["name:contains"] = args.query; // server-side when supported
|
|
120
|
-
const page = await this.np.get("/application", query);
|
|
121
|
-
return (page.results ?? []).map((app) => this.mapApp(app, namespace));
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
|
-
});
|
|
132
|
+
// `/application` is namespace-scoped with no name filter, so page each namespace to completion
|
|
133
|
+
// and filter by name client-side (below). This lifts the old silent 200-per-org cap.
|
|
134
|
+
const perNamespace = await pmap(namespaces, async (namespace) => fetchPaged((offset, limit) => this.np
|
|
135
|
+
.get("/application", { namespace_id: namespace.id, offset, limit })
|
|
136
|
+
.then((page) => page.results ?? [])
|
|
137
|
+
.catch(() => []), MAX_APPS).then((rows) => rows.map((app) => this.mapApp(app, namespace))));
|
|
127
138
|
const seen = new Set();
|
|
128
139
|
return perNamespace
|
|
129
140
|
.flat()
|
package/dist/np/journey.js
CHANGED
|
@@ -11,13 +11,22 @@ const TERMINAL = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
export const isDeploymentTerminal = (status) => !!status && TERMINAL.has(status);
|
|
13
13
|
export function bumpSemver(semver) {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Preserve the optional `v` prefix: an org that enforces a v-prefixed version pattern
|
|
15
|
+
// rejects a bare bump, and dropping it makes the release history inconsistent.
|
|
16
|
+
const parts = /^(v?)(\d+)\.(\d+)\.(\d+)/.exec(semver ?? "");
|
|
17
|
+
return parts ? `${parts[1]}${parts[2]}.${parts[3]}.${Number(parts[4]) + 1}` : "0.0.1";
|
|
16
18
|
}
|
|
17
|
-
export async function listBuilds(np, applicationId,
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
export async function listBuilds(np, applicationId, options = {}) {
|
|
20
|
+
const query = {
|
|
21
|
+
application_id: applicationId,
|
|
22
|
+
sort: "created_at:desc",
|
|
23
|
+
limit: options.limit ?? 10,
|
|
24
|
+
};
|
|
25
|
+
// The public API filters builds by full commit SHA via the `commit.id` query param (in the
|
|
26
|
+
// canonical OpenAPI contract) — this is how a change is traced from a commit to its build.
|
|
27
|
+
if (options.commit)
|
|
28
|
+
query["commit.id"] = options.commit;
|
|
29
|
+
const page = await np.get("/build", query).catch(() => ({ results: [] }));
|
|
21
30
|
return (page.results ?? []).map((build) => ({
|
|
22
31
|
id: build.id,
|
|
23
32
|
status: build.status,
|
|
@@ -6,7 +6,9 @@ import { tools } from "../tools/index.js";
|
|
|
6
6
|
*/
|
|
7
7
|
const INSTRUCTIONS = `nullplatform is where this code gets built, released, deployed and observed — these tools replace its web dashboard for the everyday developer journey.
|
|
8
8
|
|
|
9
|
-
The tools are repo-aware: inside a git repo, omit \`app\` and the linked application is inferred from the git remote. Start with \`application_get\` — it shows what's live where and suggests the next action.
|
|
9
|
+
The tools are repo-aware: inside a git repo, omit \`app\` and the linked application is inferred from the git remote. "This app", or a request that names nothing, means the repo's app — infer it; pass \`app\` only when the user means a different, explicitly named application. Start with \`application_get\` — it shows what's live where and suggests the next action.
|
|
10
|
+
|
|
11
|
+
You run in the developer's own environment, so fuse the local repo with platform state. Read the git remote, branch, HEAD commit, the diff being shipped, and config files (\`.env\`, \`package.json\`, Dockerfile), and correlate them with what the platform reports: does the local HEAD match a built or released commit, does a local config value match what a scope resolves. That correlation is this integration's edge over the web dashboard, which only sees the platform half.
|
|
10
12
|
|
|
11
13
|
Every tool accepts \`language\`: ALWAYS set it to the language the user is conversing in (ISO code, e.g. "es", "en") — answers come back in the user's language.
|
|
12
14
|
|
package/dist/tools/builds.js
CHANGED
|
@@ -12,14 +12,18 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
12
12
|
*/
|
|
13
13
|
export const buildsTool = defineTool({
|
|
14
14
|
name: TOOL.applicationBuildList,
|
|
15
|
-
title: "
|
|
16
|
-
description: "List an application's recent CI builds — status, branch, commit, age, and whether each is already released. Use it to answer 'did my push build yet?' and to pick a build_id to deploy. Pass build:<id> to see that build's assets.",
|
|
15
|
+
title: "Builds",
|
|
16
|
+
description: "List an application's recent CI builds — status, branch, commit, age, and whether each is already released. Use it to answer 'did my push build yet?' and to pick a build_id to deploy. Pass build:<id> to see that build's assets, or commit:<sha> to find the build for a specific commit (tracing a change to its build and release).",
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
18
|
errorKey: "builds.errorLabel",
|
|
19
19
|
inputSchema: {
|
|
20
20
|
app: appArg,
|
|
21
21
|
build: z.number().optional().describe("Build id — show this build's assets instead of the list"),
|
|
22
22
|
limit: z.number().optional().describe("How many recent builds (default 10)"),
|
|
23
|
+
commit: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Full commit SHA — list only the build(s) for that commit (tracing a change to its build/release)"),
|
|
23
27
|
},
|
|
24
28
|
async handler(args, context) {
|
|
25
29
|
const resolved = await requireApp(context, args);
|
|
@@ -43,11 +47,14 @@ export const buildsTool = defineTool({
|
|
|
43
47
|
return reply(markdown, { build_id: args.build, assets });
|
|
44
48
|
}
|
|
45
49
|
const [builds, releases] = await Promise.all([
|
|
46
|
-
listBuilds(context.np, app.id, args.limit ?? 10),
|
|
50
|
+
listBuilds(context.np, app.id, { limit: args.limit ?? 10, commit: args.commit }),
|
|
47
51
|
listReleases(context.np, app.id, { limit: 50 }),
|
|
48
52
|
]);
|
|
49
53
|
if (builds.length === 0) {
|
|
50
|
-
|
|
54
|
+
const empty = args.commit
|
|
55
|
+
? translate("builds.noneForCommit", { app: app.name, commit: shortCommit(args.commit) })
|
|
56
|
+
: translate("builds.none", { app: app.name }) + next(translate("builds.noneHint"));
|
|
57
|
+
return reply(empty, { app: `#${app.id}`, app_name: app.name, builds: [], commit: args.commit ?? null });
|
|
51
58
|
}
|
|
52
59
|
const releasedBuildIds = new Set(releases.map((release) => release.build_id).filter(Boolean));
|
|
53
60
|
const releaseByBuild = new Map(releases.map((release) => [release.build_id, release]));
|
|
@@ -24,7 +24,7 @@ export const createReleaseTool = defineTool({
|
|
|
24
24
|
let buildId = args.build_id;
|
|
25
25
|
let buildNote = "";
|
|
26
26
|
if (!buildId) {
|
|
27
|
-
const builds = await listBuilds(context.np, app.id, 5);
|
|
27
|
+
const builds = await listBuilds(context.np, app.id, { limit: 5 });
|
|
28
28
|
const successful = builds.find((build) => build.status === "successful");
|
|
29
29
|
if (!successful)
|
|
30
30
|
return fail(translate("createRelease.noBuilds", { app: app.name }));
|
package/dist/tools/deploy.js
CHANGED
|
@@ -15,7 +15,7 @@ async function resolveRelease(context, applicationId, args) {
|
|
|
15
15
|
// code under its name — so search deep enough that only pathologically active apps miss.
|
|
16
16
|
const [releases, builds] = await Promise.all([
|
|
17
17
|
listReleases(context.np, applicationId, { status: "active", limit: 200 }),
|
|
18
|
-
listBuilds(context.np, applicationId, 5),
|
|
18
|
+
listBuilds(context.np, applicationId, { limit: 5 }),
|
|
19
19
|
]);
|
|
20
20
|
const successfulBuild = builds.find((build) => build.status === "successful");
|
|
21
21
|
if (args.release_id) {
|
package/dist/tools/find-apps.js
CHANGED
|
@@ -7,7 +7,7 @@ import { defineTool, reply } from "../tool.js";
|
|
|
7
7
|
import { TOOL } from "../tool-names.js";
|
|
8
8
|
export const findAppsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationList,
|
|
10
|
-
title: "
|
|
10
|
+
title: "Applications",
|
|
11
11
|
description: "Search applications across the whole organization by partial name (and optionally namespace). Fast (parallel + cached). Use when the repo isn't linked or the user names an app you don't know.",
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "find-apps",
|
package/dist/tools/logs.js
CHANGED
|
@@ -7,7 +7,7 @@ import { TOOL } from "../tool-names.js";
|
|
|
7
7
|
import { appArg, chooseScope, requireApp } from "./shared.js";
|
|
8
8
|
export const logsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationLogList,
|
|
10
|
-
title: "
|
|
10
|
+
title: "Logs",
|
|
11
11
|
description: "Read recent application logs (optionally for one scope). Returns the latest lines, newest last — good for a quick 'why is it failing?' look.",
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "logs",
|
package/dist/tools/params.js
CHANGED
|
@@ -6,7 +6,7 @@ import { TOOL } from "../tool-names.js";
|
|
|
6
6
|
import { appArg, requireApp } from "./shared.js";
|
|
7
7
|
export const paramsTool = defineTool({
|
|
8
8
|
name: TOOL.applicationParameterList,
|
|
9
|
-
title: "
|
|
9
|
+
title: "Parameters",
|
|
10
10
|
description: "List an application's configuration parameters (env vars / files; secret values are masked). Use application_parameter_create to add or change them.",
|
|
11
11
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
12
12
|
widget: "params",
|
package/dist/tools/playbook.js
CHANGED
|
@@ -13,8 +13,8 @@ import { TOOL } from "../tool-names.js";
|
|
|
13
13
|
const playbookBody = (markdown) => markdown.replace(/^---\n[\s\S]*?\n---\n?/, "").trim();
|
|
14
14
|
export const playbookGetTool = defineTool({
|
|
15
15
|
name: TOOL.playbookGet,
|
|
16
|
-
title: "
|
|
17
|
-
description: "Read a nullplatform operating playbook — the methodology to follow
|
|
16
|
+
title: "Operating playbooks",
|
|
17
|
+
description: "Read a nullplatform operating playbook — the methodology to follow BEFORE the matching non-trivial work (e.g. deploying-safely before a deploy, incident-response when something is broken, configuring-safely before changing parameters or secrets). The server instructions carry the full catalog; call this with no name to list them too.",
|
|
18
18
|
annotations: { readOnlyHint: true },
|
|
19
19
|
errorKey: "playbook.errorLabel",
|
|
20
20
|
inputSchema: {
|
package/dist/tools/status.js
CHANGED
|
@@ -26,7 +26,7 @@ async function scopeViews(context, applicationId) {
|
|
|
26
26
|
}
|
|
27
27
|
export const statusTool = defineTool({
|
|
28
28
|
name: TOOL.applicationGet,
|
|
29
|
-
title: "
|
|
29
|
+
title: "Application status",
|
|
30
30
|
description: "THE place to start. Shows an application's full picture: scopes with what's live on each (release + traffic), latest build, latest release, and the one obvious next action. Call with no arguments inside a repo to use the linked app. Pass deployment:<id> to watch one rollout in detail.",
|
|
31
31
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
32
32
|
widget: "np-panel",
|
|
@@ -54,7 +54,7 @@ export const statusTool = defineTool({
|
|
|
54
54
|
return resolved.out;
|
|
55
55
|
const [views, builds, releases, orgSlug] = await Promise.all([
|
|
56
56
|
scopeViews(context, resolved.app.id),
|
|
57
|
-
listBuilds(context.np, resolved.app.id, 5),
|
|
57
|
+
listBuilds(context.np, resolved.app.id, { limit: 5 }),
|
|
58
58
|
listReleases(context.np, resolved.app.id, { limit: 5 }),
|
|
59
59
|
context.org.organizationSlug(),
|
|
60
60
|
]);
|
package/dist/tools/traffic.js
CHANGED
|
@@ -8,8 +8,8 @@ import { TOOL } from "../tool-names.js";
|
|
|
8
8
|
import { appArg, delays, requireApp, sleep } from "./shared.js";
|
|
9
9
|
export const trafficTool = defineTool({
|
|
10
10
|
name: TOOL.applicationDeploymentUpdate,
|
|
11
|
-
title: "
|
|
12
|
-
description: 'Drive an in-flight rollout: move traffic to the new version (percent snaps to 1,5,10,25,50,75,90,95,99,100), finalize it (action:"finalize" — retire the old version), or roll it back (action:"rollback" — traffic returns to the old version). Finds the app\'s active deployment automatically.',
|
|
11
|
+
title: "Update traffic",
|
|
12
|
+
description: 'Drive an in-flight rollout: move traffic to the new version (percent snaps to 0,1,5,10,25,50,75,90,95,99,100), finalize it (action:"finalize" — retire the old version), or roll it back (action:"rollback" — traffic returns to the old version). Finds the app\'s active deployment automatically.',
|
|
13
13
|
annotations: { destructiveHint: true, openWorldHint: true },
|
|
14
14
|
widget: "np-panel",
|
|
15
15
|
errorKey: "traffic.errorLabel",
|
package/package.json
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: configuring-safely
|
|
3
|
+
description: Use when setting, changing, or rotating an application's parameters or secrets (env vars / files) on nullplatform — what is safe, what only takes effect on the next deploy, and how to rotate without downtime.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Configuring safely on nullplatform
|
|
7
|
+
|
|
8
|
+
A parameter change is **inert until you redeploy**. Setting it is half the job; the value
|
|
9
|
+
reaches running instances only when each scope that uses it deploys again. Secret values are
|
|
10
|
+
encrypted and shown masked — but they are versioned, not write-once.
|
|
11
|
+
|
|
12
|
+
## What a parameter is
|
|
13
|
+
|
|
14
|
+
- An environment variable or a file (`type:"ENVIRONMENT"|"FILE"`), defined on the application
|
|
15
|
+
and resolved per scope at deploy time. Mark credentials `secret:true`.
|
|
16
|
+
- `application_parameter_create` is an upsert and convergent: re-running with the same name
|
|
17
|
+
changes the value (it reports created vs updated) and never duplicates. Each change is kept
|
|
18
|
+
as a new version platform-side (history lives in the dashboard).
|
|
19
|
+
- Secret values are masked in `application_parameter_list` and in tool output. Revealing the
|
|
20
|
+
real value needs the `parameter:read-secrets` permission (often itself approval-gated) and is
|
|
21
|
+
done in the dashboard — this integration never prints a secret back.
|
|
22
|
+
|
|
23
|
+
## Setting a value
|
|
24
|
+
|
|
25
|
+
1. Treat the repo as the source of truth for what the value should be — a local `.env` or config
|
|
26
|
+
file is the usual reference. Never paste a secret's value into chat, a commit, or a log.
|
|
27
|
+
2. `application_parameter_create` writes the **application-level** value. Per-scope and
|
|
28
|
+
per-dimension overrides (a different value in production) are managed in the dashboard. So if
|
|
29
|
+
a value seems not to take effect, a more specific override may be shadowing it — the effective
|
|
30
|
+
value is the most specific: scope override → dimension match → application default.
|
|
31
|
+
|
|
32
|
+
## Making it live (the step that's easy to forget)
|
|
33
|
+
|
|
34
|
+
3. Nothing changes until each scope redeploys. List the scopes with `application_get` and
|
|
35
|
+
redeploy every one that should pick the value up — metric-gated, per `deploying-safely`.
|
|
36
|
+
Reporting "parameter updated" without the pending redeploy is misleading.
|
|
37
|
+
|
|
38
|
+
## Rotating a secret
|
|
39
|
+
|
|
40
|
+
4. There is no in-place rotate. Set the new value (`application_parameter_create`, same name) →
|
|
41
|
+
redeploy **every** scope that uses it → verify each is healthy → only then revoke the old
|
|
42
|
+
credential upstream. Order matters: a scope left un-redeployed is still serving the old value,
|
|
43
|
+
and revoking it first breaks that scope.
|
|
44
|
+
|
|
45
|
+
## Gotchas & don'ts
|
|
46
|
+
|
|
47
|
+
- A parameter's `type`, `name`, and `secret` flag are fixed once created. To change those, create
|
|
48
|
+
a new parameter and remove the old one (then redeploy).
|
|
49
|
+
- Don't echo a secret value anywhere. Don't treat a value as live before the redeploy. Don't
|
|
50
|
+
assume the application-level value is what production serves — check for a scope override first.
|
|
@@ -16,6 +16,8 @@ over in steps you control, and nothing is destroyed until you finalize. Rollback
|
|
|
16
16
|
rejects overlapping deploys).
|
|
17
17
|
- What will ship: if the latest successful build is newer than the latest release,
|
|
18
18
|
`application_deployment_create` will cut the release automatically — say so before doing it.
|
|
19
|
+
Running in the repo, read `git log` / the diff since the live release's commit to tell the
|
|
20
|
+
user what this deploy actually changes.
|
|
19
21
|
- The target scope: when several exist, pick by dimension (`scope:"environment=production"`),
|
|
20
22
|
never by guessing. Production-dimension scopes deserve a confirmation from the user.
|
|
21
23
|
2. If the change involved parameters (`application_parameter_create`), remember values only apply on this
|
|
@@ -30,7 +32,7 @@ over in steps you control, and nothing is destroyed until you finalize. Rollback
|
|
|
30
32
|
(dashboard → Approvals). Don't retry; watch with `application_get deployment:<id>`.
|
|
31
33
|
2. Wait for `running` (instances healthy). `waiting_for_instances` lasting more than ~5
|
|
32
34
|
minutes usually means a failing health check — check `application_log_list` before pushing traffic.
|
|
33
|
-
3. Walk traffic in steps — allowed marks are 1, 5, 10, 25, 50, 75, 90, 95, 99, 100:
|
|
35
|
+
3. Walk traffic in steps — allowed marks are 0, 1, 5, 10, 25, 50, 75, 90, 95, 99, 100:
|
|
34
36
|
- Low-risk/dev: 25 → 100 is fine.
|
|
35
37
|
- Production: 5 or 10 → 25 → 50 → 100, pausing 2–5 minutes per step.
|
|
36
38
|
4. **Gate every step on metrics** (`application_metric_list`, 1h window, compare against pre-deploy):
|
|
@@ -50,5 +52,6 @@ Prefer a needless rollback over a defended bad deploy. After rolling back, captu
|
|
|
50
52
|
## First-ever deploy on a scope
|
|
51
53
|
|
|
52
54
|
There is no old version to fall back to: traffic semantics don't apply, the deployment
|
|
53
|
-
finalizes on its own once instances are healthy, and "rollback" means cancel
|
|
54
|
-
the scope
|
|
55
|
+
finalizes on its own once instances are healthy, and "rollback" means cancel — which ends in
|
|
56
|
+
`failed`, leaving the scope to be recreated rather than reverted. Verify with the scope's
|
|
57
|
+
domain URL from `application_get` once finalized.
|
|
@@ -34,6 +34,8 @@ to the last known-good version.
|
|
|
34
34
|
- throughput cliff to ~0 → upstream/routing problem; the app may be healthy.
|
|
35
35
|
- `application_log_list` (the affected scope; filter for `ERROR`, `FATAL`, stack traces):
|
|
36
36
|
- note the FIRST error timestamp and correlate with the deploy time from `application_get`.
|
|
37
|
+
- running in the repo: map the suspect release to its build commit and read `git log` around
|
|
38
|
+
it — the changes between the last-good and the current release are the prime suspects.
|
|
37
39
|
- `application_parameter_list` — config is a top cause: was a parameter changed before the last deploy?
|
|
38
40
|
Secret values are masked, but names + the deploy timing tell the story.
|
|
39
41
|
|
|
@@ -47,6 +49,7 @@ and the evidence — and leave the bad release identified so it isn't redeployed
|
|
|
47
49
|
## Don'ts
|
|
48
50
|
|
|
49
51
|
- Don't deploy a "quick fix" forward while the incident is open — roll back first.
|
|
50
|
-
- Don't finalize
|
|
52
|
+
- Don't finalize during an incident — once a deploy is `finalizing` it can't be cancelled or
|
|
53
|
+
rolled back; it only retries forward.
|
|
51
54
|
- Don't restart/recreate scopes as a first move; that destroys the evidence and rarely
|
|
52
55
|
fixes the cause.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: managing-environments
|
|
3
|
+
description: Use when adding or changing a nullplatform deploy target (scope / environment / region) — identifying existing scopes, carrying the org's dimensions, and the first deploy to a fresh scope.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Managing environments on nullplatform
|
|
7
|
+
|
|
8
|
+
A scope IS an environment, and creating one **provisions real infrastructure asynchronously**.
|
|
9
|
+
Identify before you create, carry the org's dimensions, and mirror a sibling.
|
|
10
|
+
|
|
11
|
+
## 1. Identify before creating
|
|
12
|
+
|
|
13
|
+
`application_get` first — list the existing scopes and their dimensions. "Add staging" often
|
|
14
|
+
means a scope that already exists; match by **dimension** (`environment=staging`), not by display
|
|
15
|
+
name. `application_scope_create` is convergent (an existing name returns that scope), but a
|
|
16
|
+
near-duplicate under a different name is a real-infrastructure mistake.
|
|
17
|
+
|
|
18
|
+
## 2. Dimensions
|
|
19
|
+
|
|
20
|
+
In orgs that use runtime dimensions a new scope must carry them (e.g. `environment`, `country`).
|
|
21
|
+
`application_scope_create` borrows the shape of an existing scope when you don't pass
|
|
22
|
+
`dimensions` — usually what you want. Set them right at creation: the dashboard locks dimensions
|
|
23
|
+
afterwards, so treat them as fixed even though the platform technically allows edits. A
|
|
24
|
+
production-dimension scope deserves an explicit confirmation from the user.
|
|
25
|
+
|
|
26
|
+
## 3. Create
|
|
27
|
+
|
|
28
|
+
`application_scope_create name:"…"` (it lists the scope types to choose from if the org has
|
|
29
|
+
several). It returns immediately with the scope **provisioning** — the infrastructure comes up
|
|
30
|
+
out of band. Poll with `application_get` until it's active before deploying; the create response
|
|
31
|
+
is accepted intent, not a ready environment.
|
|
32
|
+
|
|
33
|
+
## 4. Mirror config, then first deploy
|
|
34
|
+
|
|
35
|
+
- A new environment with missing parameters will misbehave. Bring the sibling's configuration
|
|
36
|
+
across (`application_parameter_create`, per `configuring-safely`) before the first deploy.
|
|
37
|
+
- The first deploy to a fresh scope is special: there's no previous version, so traffic-splitting
|
|
38
|
+
doesn't apply — it finalizes on its own once instances are healthy, and a cancel ends in
|
|
39
|
+
`failed` (the scope must be recreated, since there's nothing to fall back to). Verify via the
|
|
40
|
+
scope's domain URL from `application_get`.
|
|
41
|
+
|
|
42
|
+
## Sizing & later edits
|
|
43
|
+
|
|
44
|
+
Sizing/capabilities and post-creation dimension edits aren't exposed by this integration — set
|
|
45
|
+
what you can at creation and deep-link to the dashboard for the rest. An under-sized scope shows
|
|
46
|
+
up later as CPU/memory near limits in `application_metric_list`.
|
|
47
|
+
|
|
48
|
+
## Don'ts
|
|
49
|
+
|
|
50
|
+
- Don't create a duplicate of an existing scope; identify first.
|
|
51
|
+
- Don't guess dimension values — borrow a sibling's (the tool does this by default).
|
|
52
|
+
- Don't treat scope creation as cheap or instant: it's real infrastructure, provisioned
|
|
53
|
+
asynchronously.
|
|
@@ -30,29 +30,39 @@ Scopes carry org-defined dimensions like `environment` and `country`
|
|
|
30
30
|
|
|
31
31
|
## Versioning
|
|
32
32
|
|
|
33
|
-
- Releases use semver, optionally `v`-prefixed;
|
|
34
|
-
|
|
33
|
+
- Releases use semver, optionally `v`-prefixed; an org may enforce a pattern
|
|
34
|
+
(`settings.releases.versionPattern`). Whoever cuts the release chooses the semver.
|
|
35
|
+
- When a tool cuts a release for you (deploy, `application_release_create`) it defaults to
|
|
36
|
+
bumping the patch of the latest release. "Deploy 1.4.0" targets the EXISTING 1.4.0 release —
|
|
35
37
|
that's also how you roll back after a finalize.
|
|
36
38
|
|
|
37
39
|
## Parameters
|
|
38
40
|
|
|
39
|
-
- Env vars or files, app-NRN scoped, optionally secret
|
|
41
|
+
- Env vars or files, app-NRN scoped, optionally secret. Secret values are masked by default;
|
|
42
|
+
reading the real value needs the `parameter:read-secrets` permission (often itself
|
|
43
|
+
approval-gated) and is not exposed by this integration — a value is versioned, not write-once.
|
|
40
44
|
- Changes take effect ONLY on the next deploy of each scope — saying "parameter updated"
|
|
41
45
|
without mentioning the pending redeploy is misleading.
|
|
46
|
+
- The effective value for a scope is the most specific one: scope override → dimension match →
|
|
47
|
+
application default.
|
|
42
48
|
|
|
43
49
|
## Traffic & lifecycle semantics
|
|
44
50
|
|
|
45
|
-
- Traffic percentages snap to: 1, 5, 10, 25, 50, 75, 90, 95, 99, 100.
|
|
51
|
+
- Traffic percentages snap to: 0, 1, 5, 10, 25, 50, 75, 90, 95, 99, 100 (0 drains the new version).
|
|
46
52
|
- Deployment statuses: `creating → waiting_for_instances → running (traffic phase) →
|
|
47
|
-
finalizing → finalized`;
|
|
48
|
-
`
|
|
53
|
+
finalizing → finalized`; a rollback runs `cancelling → rolling_back → rolled_back`; terminal
|
|
54
|
+
also includes `failed`. `creating_approval` means a human approval gate (denied →
|
|
55
|
+
`creating_approval_denied`). A separate axis, `statusInScope` (active/candidate/inactive),
|
|
56
|
+
is what the scope actually serves. Once a deploy is `finalizing` it can't be cancelled — it
|
|
57
|
+
only retries forward.
|
|
49
58
|
- One active rollout per scope.
|
|
50
59
|
|
|
51
60
|
## Gotchas worth knowing
|
|
52
61
|
|
|
53
|
-
- Multi-asset builds (e.g. docker + lambda) need an asset choice per scope
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
- Multi-asset builds (e.g. docker + lambda) need an asset choice per scope (set the scope's
|
|
63
|
+
asset when a build emits more than one; a single-asset build needs no choice). A wrong or
|
|
64
|
+
missing choice is rejected with a misleading "scope and release belong to different
|
|
65
|
+
applications" — it actually means "pick the asset" (a real runtime error, verified live).
|
|
56
66
|
- Logs and metrics are per-scope, never app-wide.
|
|
57
67
|
- Approvals can gate deploys org-wide; a "stuck" deploy in `creating_approval` is waiting
|
|
58
68
|
for a human, not broken.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: promoting-across-environments
|
|
3
|
+
description: Use when moving a validated release from one environment to the next (dev → staging → production) on nullplatform, or when two environments behave differently — promote the same release, and tell release drift from config drift.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Promoting across environments on nullplatform
|
|
7
|
+
|
|
8
|
+
Build once, deploy the same release everywhere. A release is an immutable, application-wide
|
|
9
|
+
pointer to a build, so promoting to the next environment means **deploying the release you
|
|
10
|
+
already validated** onto that environment's scope — never rebuilding, never cutting a new release
|
|
11
|
+
per environment. What you tested is then exactly what ships.
|
|
12
|
+
|
|
13
|
+
> "Promote" here is the developer sense: move a release to the next environment. The platform
|
|
14
|
+
> also uses "promote" for *finalize* — promoting a deployment's candidate over the old version
|
|
15
|
+
> within one rollout. Different operation; don't conflate them.
|
|
16
|
+
|
|
17
|
+
## Promoting a release
|
|
18
|
+
|
|
19
|
+
1. Validate on the lower environment first. With `application_get`, confirm which release is live
|
|
20
|
+
on (say) the `environment=staging` scope and that it's healthy — metrics and logs per
|
|
21
|
+
`deploying-safely`.
|
|
22
|
+
2. Deploy that **same** release to the next scope by version:
|
|
23
|
+
`application_deployment_create version:"1.4.2" scope:"environment=production"`. Passing the
|
|
24
|
+
existing version deploys it as-is; it does not cut a new release. Do NOT deploy "latest" here —
|
|
25
|
+
a newer build may exist between staging and prod, and "latest" would ship something you never
|
|
26
|
+
validated.
|
|
27
|
+
3. Walk traffic and gate exactly as in `deploying-safely`. A production-dimension scope deserves
|
|
28
|
+
an explicit confirmation. Repeat environment by environment.
|
|
29
|
+
|
|
30
|
+
## When an environment behaves differently (drift)
|
|
31
|
+
|
|
32
|
+
Two scopes running "the same app" can differ in exactly two ways — establish which before acting:
|
|
33
|
+
|
|
34
|
+
- **Release drift** — they're on different releases. `application_get` shows the live release per
|
|
35
|
+
scope; staging on 1.4.2 and prod on 1.3.0 is the whole story. The fix is promotion (above), not
|
|
36
|
+
debugging.
|
|
37
|
+
- **Config drift** — same release, different configuration. A parameter can be overridden per
|
|
38
|
+
scope or dimension (effective value: scope → dimension → application), so prod may resolve a
|
|
39
|
+
different value than staging. `application_parameter_list` shows the names; per-scope override
|
|
40
|
+
values live in the dashboard. Fuse with the repo — a local `.env` or config file is the
|
|
41
|
+
reference for what each environment *should* carry.
|
|
42
|
+
|
|
43
|
+
Promoting a release won't fix a config difference, and changing config won't close a release gap —
|
|
44
|
+
so name the drift before you touch anything.
|
|
45
|
+
|
|
46
|
+
## Don'ts
|
|
47
|
+
|
|
48
|
+
- Don't rebuild per environment, and don't cut a separate release for prod — promote the one you
|
|
49
|
+
validated.
|
|
50
|
+
- Don't deploy "latest" to production when you mean "the release that passed staging".
|
|
51
|
+
- Don't assume parity: two scopes can drift on release, on config, or both.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tracing-a-change
|
|
3
|
+
description: Use to answer "is this ticket / PR / commit live, and where?" on nullplatform — walk a commit from the issue key to the scopes serving it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tracing a change on nullplatform
|
|
7
|
+
|
|
8
|
+
Intent and delivery join on the **commit**. Given a ticket, a PR, or a commit, walk it forward to
|
|
9
|
+
the environments actually serving it — and be precise about the gap, because *merged* is not
|
|
10
|
+
*deployed*.
|
|
11
|
+
|
|
12
|
+
## The walk
|
|
13
|
+
|
|
14
|
+
1. **Work item → commit.** Get the commit SHA the change landed in: from the issue's linked PR in
|
|
15
|
+
the tracker, or in the repo with `git log --grep="PROJ-123"` (the issue key on the commit). Use
|
|
16
|
+
the full SHA.
|
|
17
|
+
2. **Commit → build.** `application_build_list commit:"<sha>"` returns the build(s) for that exact
|
|
18
|
+
commit (a build records its commit). No build → it hasn't built yet, or the commit isn't on a
|
|
19
|
+
built branch.
|
|
20
|
+
3. **Build → release.** A build ships only once someone has cut a release from it;
|
|
21
|
+
`application_build_list` flags whether each build is released and with which semver.
|
|
22
|
+
4. **Release → scopes.** `application_get` shows the live release per scope. If the change's
|
|
23
|
+
release is on staging but not production, that is the precise answer — and the next action is
|
|
24
|
+
`promoting-across-environments`.
|
|
25
|
+
|
|
26
|
+
## Answer with a position, not a yes/no
|
|
27
|
+
|
|
28
|
+
State where the change *is*: "PROJ-123 is in release 1.4.2 — live on staging since Tuesday, not
|
|
29
|
+
yet on production." If it hasn't built or been released, say which step it's stuck at. Never equate
|
|
30
|
+
a merged PR with a deployed change; the gap between them is the whole point of the trace.
|