@intentius/chant-lexicon-temporal 0.1.5
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/integrity.json +15 -0
- package/dist/manifest.json +8 -0
- package/dist/meta.json +26 -0
- package/dist/rules/tmp001.ts +53 -0
- package/dist/rules/tmp002.ts +45 -0
- package/dist/rules/tmp010-cron-syntax.ts +64 -0
- package/dist/rules/tmp011-namespace-reference.ts +49 -0
- package/dist/skills/chant-temporal-ops.md +184 -0
- package/dist/skills/chant-temporal.md +201 -0
- package/dist/types/index.d.ts +3 -0
- package/package.json +32 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +23 -0
- package/src/codegen/generate-cli.ts +17 -0
- package/src/codegen/generate.ts +82 -0
- package/src/codegen/package-cli.ts +14 -0
- package/src/codegen/package.ts +55 -0
- package/src/composites/cloud-stack.ts +74 -0
- package/src/composites/composites.test.ts +131 -0
- package/src/composites/dev-stack.ts +81 -0
- package/src/config.ts +150 -0
- package/src/coverage.test.ts +9 -0
- package/src/coverage.ts +37 -0
- package/src/example.test.ts +59 -0
- package/src/generated/lexicon-temporal.json +26 -0
- package/src/index.ts +29 -0
- package/src/lint/post-synth/post-synth.test.ts +152 -0
- package/src/lint/post-synth/tmp010-cron-syntax.ts +64 -0
- package/src/lint/post-synth/tmp011-namespace-reference.ts +49 -0
- package/src/lint/rules/index.ts +2 -0
- package/src/lint/rules/lint-rules.test.ts +150 -0
- package/src/lint/rules/tmp001.ts +53 -0
- package/src/lint/rules/tmp002.ts +45 -0
- package/src/plugin.test.ts +97 -0
- package/src/plugin.ts +286 -0
- package/src/resources.ts +121 -0
- package/src/serializer.test.ts +292 -0
- package/src/serializer.ts +310 -0
- package/src/skills/chant-temporal-ops.md +184 -0
- package/src/skills/chant-temporal.md +201 -0
- package/src/validate-cli.ts +7 -0
- package/src/validate.test.ts +9 -0
- package/src/validate.ts +92 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"algorithm": "sha256",
|
|
3
|
+
"artifacts": {
|
|
4
|
+
"manifest.json": "6e636e68d039c23e7a244602a31d0cd38192b7cbe719aabff99eb7d485c71291",
|
|
5
|
+
"meta.json": "c77a3c415993bed3865e07fa6db4fb7b9e87de0afa8d8675f64eadf50f914077",
|
|
6
|
+
"types/index.d.ts": "5216f5256321f084a7c8211ef66ab599f621c74751cc3485cfc2a62502b81e2f",
|
|
7
|
+
"rules/tmp001.ts": "689222211a93716a7e4432f6dd6a2a2ab54020b232049fb602893872585f9978",
|
|
8
|
+
"rules/tmp002.ts": "a227dc8c9aae16bbb468664a35a199c0880827b15075227c8e26a8c5fe2eff30",
|
|
9
|
+
"rules/tmp010-cron-syntax.ts": "47db78e7595bcd24a9a735dfd738e11b2d3d9465e317ae836fb3da8ff74cee63",
|
|
10
|
+
"rules/tmp011-namespace-reference.ts": "221c7832ab3012c1cf1e3a9f4a7f6fa8958b85b5fc0c75ab9673c920cbc97833",
|
|
11
|
+
"skills/chant-temporal.md": "ff929983b33bb420c49ab61851f3799c5610256cb972d6c584792bb98b0cfb22",
|
|
12
|
+
"skills/chant-temporal-ops.md": "7e83bc3e6e63af4ab14b8dc07aa00132d51991cf35b1bca29560d48934bff6dc"
|
|
13
|
+
},
|
|
14
|
+
"composite": "ead26ea60623f339408c9a74f60a55a506a2d5b0f6fafc041eefc2ca92bb5b04"
|
|
15
|
+
}
|
package/dist/meta.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"TemporalServer": {
|
|
3
|
+
"resourceType": "Temporal::Server",
|
|
4
|
+
"kind": "resource",
|
|
5
|
+
"lexicon": "temporal",
|
|
6
|
+
"description": "Temporal server deployment — emits docker-compose.yml and Helm values"
|
|
7
|
+
},
|
|
8
|
+
"TemporalNamespace": {
|
|
9
|
+
"resourceType": "Temporal::Namespace",
|
|
10
|
+
"kind": "resource",
|
|
11
|
+
"lexicon": "temporal",
|
|
12
|
+
"description": "Temporal namespace — emits namespace create command in temporal-setup.sh"
|
|
13
|
+
},
|
|
14
|
+
"SearchAttribute": {
|
|
15
|
+
"resourceType": "Temporal::SearchAttribute",
|
|
16
|
+
"kind": "resource",
|
|
17
|
+
"lexicon": "temporal",
|
|
18
|
+
"description": "Temporal search attribute — emits search-attribute create command in temporal-setup.sh"
|
|
19
|
+
},
|
|
20
|
+
"TemporalSchedule": {
|
|
21
|
+
"resourceType": "Temporal::Schedule",
|
|
22
|
+
"kind": "resource",
|
|
23
|
+
"lexicon": "temporal",
|
|
24
|
+
"description": "Temporal schedule — emits SDK schedule creation TypeScript to schedules/<id>.ts"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMP001: TemporalNamespace retention too short
|
|
3
|
+
*
|
|
4
|
+
* Workflow history older than the retention period is permanently deleted.
|
|
5
|
+
* Retentions shorter than 3 days leave very little time for debugging
|
|
6
|
+
* failures or running ad-hoc queries against closed workflow executions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
10
|
+
|
|
11
|
+
/** Parse a retention string like "1d", "12h", "3d" → total hours. Returns NaN on unrecognised format. */
|
|
12
|
+
function retentionHours(retention: string): number {
|
|
13
|
+
const days = /^(\d+)d$/i.exec(retention);
|
|
14
|
+
if (days) return Number(days[1]) * 24;
|
|
15
|
+
const hours = /^(\d+)h$/i.exec(retention);
|
|
16
|
+
if (hours) return Number(hours[1]);
|
|
17
|
+
return NaN;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const tmp001: LintRule = {
|
|
21
|
+
id: "TMP001",
|
|
22
|
+
severity: "error",
|
|
23
|
+
category: "correctness",
|
|
24
|
+
description: "TemporalNamespace retention should be at least 3 days to preserve workflow history for debugging",
|
|
25
|
+
|
|
26
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
27
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [name, entity] of context.entities) {
|
|
30
|
+
const et = (entity as Record<string, unknown>).entityType as string;
|
|
31
|
+
if (et !== "Temporal::Namespace") continue;
|
|
32
|
+
|
|
33
|
+
const props = (entity as { props?: Record<string, unknown> }).props ?? {};
|
|
34
|
+
const retention = props.retention as string | undefined;
|
|
35
|
+
if (!retention) continue; // default "7d" — not an error
|
|
36
|
+
|
|
37
|
+
const hours = retentionHours(retention);
|
|
38
|
+
if (isNaN(hours)) continue; // unrecognised format — skip
|
|
39
|
+
|
|
40
|
+
if (hours < 72) {
|
|
41
|
+
diagnostics.push({
|
|
42
|
+
ruleId: "TMP001",
|
|
43
|
+
severity: "error",
|
|
44
|
+
message: `Namespace "${name}" has retention "${retention}" — minimum recommended is 3d (72h) to preserve workflow history`,
|
|
45
|
+
entity: name,
|
|
46
|
+
fix: 'Set retention to at least "3d" — e.g. retention: "7d"',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return diagnostics;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMP002: TemporalSchedule AllowAll overlap without explanatory note
|
|
3
|
+
*
|
|
4
|
+
* AllowAll allows any number of concurrent schedule runs. This is safe for
|
|
5
|
+
* idempotent, read-only workflows, but can cause resource exhaustion or
|
|
6
|
+
* duplicate side-effects if not explicitly intended. Requiring a note forces
|
|
7
|
+
* the author to document the intent.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
11
|
+
|
|
12
|
+
export const tmp002: LintRule = {
|
|
13
|
+
id: "TMP002",
|
|
14
|
+
severity: "warning",
|
|
15
|
+
category: "best-practices",
|
|
16
|
+
description: "TemporalSchedule with overlap AllowAll should include state.note explaining the intent",
|
|
17
|
+
|
|
18
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
19
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [name, entity] of context.entities) {
|
|
22
|
+
const et = (entity as Record<string, unknown>).entityType as string;
|
|
23
|
+
if (et !== "Temporal::Schedule") continue;
|
|
24
|
+
|
|
25
|
+
const props = (entity as { props?: Record<string, unknown> }).props ?? {};
|
|
26
|
+
const policies = props.policies as Record<string, unknown> | undefined;
|
|
27
|
+
if (policies?.overlap !== "AllowAll") continue;
|
|
28
|
+
|
|
29
|
+
const state = props.state as Record<string, unknown> | undefined;
|
|
30
|
+
const note = state?.note as string | undefined;
|
|
31
|
+
|
|
32
|
+
if (!note || note.trim() === "") {
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
ruleId: "TMP002",
|
|
35
|
+
severity: "warning",
|
|
36
|
+
message: `Schedule "${name}" uses overlap "AllowAll" — add state.note explaining why concurrent runs are safe`,
|
|
37
|
+
entity: name,
|
|
38
|
+
fix: 'Add state: { note: "Workflow is idempotent — concurrent runs are safe" }',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return diagnostics;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMP010: TemporalSchedule cron expression syntax
|
|
3
|
+
*
|
|
4
|
+
* Validates that cron expressions in TemporalSchedule.spec.cronExpressions
|
|
5
|
+
* are valid 5- or 6-field cron syntax. Malformed crons are silently ignored
|
|
6
|
+
* by Temporal's scheduler, leading to schedules that never fire.
|
|
7
|
+
*
|
|
8
|
+
* This is a pre-submission guard — final validation is Temporal's own parser.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
|
+
|
|
13
|
+
/** Very permissive cron field pattern — catches obvious syntax errors. */
|
|
14
|
+
const CRON_FIELD = /^[0-9*,/\-?LW#]+$/;
|
|
15
|
+
|
|
16
|
+
function isValidCronExpression(expr: string): boolean {
|
|
17
|
+
const fields = expr.trim().split(/\s+/);
|
|
18
|
+
if (fields.length < 5 || fields.length > 6) return false;
|
|
19
|
+
return fields.every((f) => CRON_FIELD.test(f));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const tmp010: PostSynthCheck = {
|
|
23
|
+
id: "TMP010",
|
|
24
|
+
description: "TemporalSchedule cron expressions must be valid 5- or 6-field cron syntax",
|
|
25
|
+
|
|
26
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [lexicon, output] of ctx.outputs) {
|
|
30
|
+
if (lexicon !== "temporal") continue;
|
|
31
|
+
|
|
32
|
+
// Schedules emit individual TypeScript files — check the schedules/ files.
|
|
33
|
+
const files =
|
|
34
|
+
typeof output === "string"
|
|
35
|
+
? new Map<string, string>()
|
|
36
|
+
: (output as { primary: string; files?: Record<string, string> }).files
|
|
37
|
+
? new Map(Object.entries((output as { files: Record<string, string> }).files))
|
|
38
|
+
: new Map<string, string>();
|
|
39
|
+
|
|
40
|
+
for (const [filename, content] of files) {
|
|
41
|
+
if (!filename.startsWith("schedules/")) continue;
|
|
42
|
+
|
|
43
|
+
// Extract cron expressions from the generated TypeScript:
|
|
44
|
+
// cronExpressions: ["0 3 * * *"]
|
|
45
|
+
const cronMatches = [...content.matchAll(/cronExpressions:\s*\[([^\]]+)\]/g)];
|
|
46
|
+
for (const match of cronMatches) {
|
|
47
|
+
const exprs = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
48
|
+
for (const expr of exprs) {
|
|
49
|
+
if (!isValidCronExpression(expr)) {
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
checkId: "TMP010",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message: `${filename}: cron expression "${expr}" does not look like valid 5- or 6-field cron syntax`,
|
|
54
|
+
lexicon: "temporal",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return diagnostics;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMP011: SearchAttribute references an undeclared namespace
|
|
3
|
+
*
|
|
4
|
+
* If SearchAttribute.namespace is set to a value X, there must be a
|
|
5
|
+
* TemporalNamespace entity with name === X in the project. A missing
|
|
6
|
+
* namespace means the search attribute create command will fail at runtime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
|
|
11
|
+
export const tmp011: PostSynthCheck = {
|
|
12
|
+
id: "TMP011",
|
|
13
|
+
description: "SearchAttribute.namespace must reference a declared TemporalNamespace entity",
|
|
14
|
+
|
|
15
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
16
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
17
|
+
|
|
18
|
+
// Collect declared namespace names
|
|
19
|
+
const declaredNamespaces = new Set<string>();
|
|
20
|
+
for (const [, entity] of ctx.entities) {
|
|
21
|
+
const et = (entity as Record<string, unknown>).entityType as string;
|
|
22
|
+
if (et !== "Temporal::Namespace") continue;
|
|
23
|
+
const props = (entity as { props?: Record<string, unknown> }).props ?? {};
|
|
24
|
+
const name = props.name as string | undefined;
|
|
25
|
+
if (name) declaredNamespaces.add(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check each SearchAttribute that specifies a namespace
|
|
29
|
+
for (const [entityKey, entity] of ctx.entities) {
|
|
30
|
+
const et = (entity as Record<string, unknown>).entityType as string;
|
|
31
|
+
if (et !== "Temporal::SearchAttribute") continue;
|
|
32
|
+
|
|
33
|
+
const props = (entity as { props?: Record<string, unknown> }).props ?? {};
|
|
34
|
+
const ns = props.namespace as string | undefined;
|
|
35
|
+
if (!ns) continue; // no namespace specified — applies globally, OK
|
|
36
|
+
|
|
37
|
+
if (!declaredNamespaces.has(ns)) {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
checkId: "TMP011",
|
|
40
|
+
severity: "error",
|
|
41
|
+
message: `SearchAttribute "${entityKey}" references namespace "${ns}" which is not declared — add a TemporalNamespace with name "${ns}"`,
|
|
42
|
+
lexicon: "temporal",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return diagnostics;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-temporal-ops
|
|
3
|
+
description: Signal workflows, diagnose stuck activities, reset checkpoints, and cancel runs via the Temporal CLI and chant run
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Temporal Operations Playbook
|
|
8
|
+
|
|
9
|
+
## Signal a gate (unblock a paused workflow)
|
|
10
|
+
|
|
11
|
+
Workflows that use `setHandler` on a signal pause at gate activities waiting for a named signal. The `chant run signal` command (available in issue #8) forwards signals to the running workflow.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Via chant CLI (requires chant run — issue #8)
|
|
15
|
+
chant run signal <op-name> dnsConfigured
|
|
16
|
+
|
|
17
|
+
# Directly via temporal CLI
|
|
18
|
+
temporal workflow signal \
|
|
19
|
+
--workflow-id <workflow-id> \
|
|
20
|
+
--name dnsConfigured \
|
|
21
|
+
--namespace <namespace>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
List pending signals by querying the workflow:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
temporal workflow query \
|
|
28
|
+
--workflow-id <workflow-id> \
|
|
29
|
+
--type currentPhase \
|
|
30
|
+
--namespace <namespace>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Check run status
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Summary view
|
|
37
|
+
temporal workflow describe --workflow-id <id> --namespace <ns>
|
|
38
|
+
|
|
39
|
+
# Full event history
|
|
40
|
+
temporal workflow show --workflow-id <id> --namespace <ns>
|
|
41
|
+
|
|
42
|
+
# Filter by search attribute (requires registered custom attributes)
|
|
43
|
+
temporal workflow list \
|
|
44
|
+
--namespace <ns> \
|
|
45
|
+
--query 'GcpProject = "my-project"'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Diagnose a stuck activity
|
|
49
|
+
|
|
50
|
+
Activities can be stuck for three distinct reasons:
|
|
51
|
+
|
|
52
|
+
| Symptom | Cause | Fix |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Activity never started | `scheduleToStartTimeout` exceeded — no available workers | Start the worker: `chant run <op>` |
|
|
55
|
+
| Activity started but no heartbeats | `heartbeatTimeout` exceeded — worker crashed mid-activity | Bounce the worker; Temporal auto-retries |
|
|
56
|
+
| Activity running but slow | Normal — long-running activities with heartbeats | Wait, or check heartbeat details |
|
|
57
|
+
|
|
58
|
+
### View activity timeout details
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
temporal workflow show --workflow-id <id> --namespace <ns> | grep -A5 "ActivityTaskScheduled"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Check worker connectivity
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# See which task queues have pollers
|
|
68
|
+
temporal task-queue describe --task-queue <queue> --namespace <ns>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A task queue with `pollerCount: 0` means no workers are running.
|
|
72
|
+
|
|
73
|
+
## Reset a workflow to a previous checkpoint
|
|
74
|
+
|
|
75
|
+
Use `workflow reset` to replay a workflow from a specific event, skipping failed activities:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Reset to just before the most recent failure
|
|
79
|
+
temporal workflow reset \
|
|
80
|
+
--workflow-id <id> \
|
|
81
|
+
--namespace <ns> \
|
|
82
|
+
--event-id <N> \
|
|
83
|
+
--reason "Retrying after infra fix"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Find the event ID to reset to:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# List events — find the last successful ActivityTaskCompleted before the failure
|
|
90
|
+
temporal workflow show --workflow-id <id> --namespace <ns> | grep -n "ActivityTask"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Reset to the beginning of a named phase (requires workflow to record phase transitions as signals or markers):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
temporal workflow reset \
|
|
97
|
+
--workflow-id <id> \
|
|
98
|
+
--namespace <ns> \
|
|
99
|
+
--reapply-type None \
|
|
100
|
+
--type LastWorkflowTask
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Cancel a stuck or unwanted run
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Graceful cancel — workflow receives CancellationError and can clean up
|
|
107
|
+
temporal workflow cancel \
|
|
108
|
+
--workflow-id <id> \
|
|
109
|
+
--namespace <ns>
|
|
110
|
+
|
|
111
|
+
# Forceful terminate — immediate stop, no cleanup
|
|
112
|
+
temporal workflow terminate \
|
|
113
|
+
--workflow-id <id> \
|
|
114
|
+
--namespace <ns> \
|
|
115
|
+
--reason "Terminated by operator"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Pause and resume a schedule
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Pause
|
|
122
|
+
temporal schedule pause \
|
|
123
|
+
--schedule-id <id> \
|
|
124
|
+
--namespace <ns> \
|
|
125
|
+
--note "Paused for maintenance"
|
|
126
|
+
|
|
127
|
+
# Resume
|
|
128
|
+
temporal schedule unpause \
|
|
129
|
+
--schedule-id <id> \
|
|
130
|
+
--namespace <ns>
|
|
131
|
+
|
|
132
|
+
# Trigger immediately (ignores spec)
|
|
133
|
+
temporal schedule trigger \
|
|
134
|
+
--schedule-id <id> \
|
|
135
|
+
--namespace <ns>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Inspect workflow history for debugging
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Show all events in JSON (machine-readable)
|
|
142
|
+
temporal workflow show \
|
|
143
|
+
--workflow-id <id> \
|
|
144
|
+
--namespace <ns> \
|
|
145
|
+
--output json
|
|
146
|
+
|
|
147
|
+
# Filter to activity failures only
|
|
148
|
+
temporal workflow show \
|
|
149
|
+
--workflow-id <id> \
|
|
150
|
+
--namespace <ns> \
|
|
151
|
+
--output json | jq '.[] | select(.eventType == "ActivityTaskFailed")'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Common failure patterns
|
|
155
|
+
|
|
156
|
+
### "no workers polling" after `chant run`
|
|
157
|
+
|
|
158
|
+
The worker started but cannot connect. Check:
|
|
159
|
+
1. `TEMPORAL_ADDRESS` matches the server address
|
|
160
|
+
2. `TEMPORAL_NAMESPACE` matches the namespace the workflow was started in
|
|
161
|
+
3. TLS and API key config match (`tls: true` + `apiKey` for Temporal Cloud)
|
|
162
|
+
|
|
163
|
+
### Activity retrying indefinitely
|
|
164
|
+
|
|
165
|
+
Default retry policy has `maximumAttempts: 0` (unlimited). If an activity is retrying unexpectedly:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Check the current attempt count and last failure
|
|
169
|
+
temporal workflow show --workflow-id <id> --namespace <ns> | grep "attempt\|failure"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Add `maximumAttempts` to the activity's retry policy in the workflow code, or cancel the run.
|
|
173
|
+
|
|
174
|
+
### "workflow execution already started" on re-run
|
|
175
|
+
|
|
176
|
+
`chant run` uses deterministic workflow IDs (e.g. `crdb-deploy-{project}`). If a previous run is still open:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Check if it's still running
|
|
180
|
+
temporal workflow describe --workflow-id <id> --namespace <ns> | grep "status"
|
|
181
|
+
|
|
182
|
+
# If stuck, terminate it first
|
|
183
|
+
temporal workflow terminate --workflow-id <id> --namespace <ns> --reason "Restarting"
|
|
184
|
+
```
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-temporal
|
|
3
|
+
description: Build and manage Temporal server deployment, namespace provisioning, and schedule registration from a chant project
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Temporal Operational Playbook
|
|
8
|
+
|
|
9
|
+
## How chant and Temporal relate
|
|
10
|
+
|
|
11
|
+
chant is a **synthesis compiler** — it compiles TypeScript resource declarations into deployment artifacts. `chant build` does not start Temporal or register anything; synthesis is pure and deterministic. Your job as an agent is to bridge synthesis and deployment:
|
|
12
|
+
|
|
13
|
+
- Use **chant** for: build, lint, diff (local config comparison)
|
|
14
|
+
- Use **docker compose / kubectl / temporal CLI** for: starting the server, applying configs, and all runtime operations
|
|
15
|
+
|
|
16
|
+
The source of truth for Temporal configuration is the TypeScript in `src/`. The generated artifacts in `dist/` are intermediate outputs.
|
|
17
|
+
|
|
18
|
+
## Resources and their outputs
|
|
19
|
+
|
|
20
|
+
| Resource | Emits |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `TemporalServer` | `docker-compose.yml` (primary) + `temporal-helm-values.yaml` |
|
|
23
|
+
| `TemporalNamespace` | `temporal-setup.sh` (namespace create commands) |
|
|
24
|
+
| `SearchAttribute` | `temporal-setup.sh` (search-attribute create commands) |
|
|
25
|
+
| `TemporalSchedule` | `schedules/<id>.ts` (SDK schedule creation script) |
|
|
26
|
+
|
|
27
|
+
## Build and validate
|
|
28
|
+
|
|
29
|
+
### Build the project
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
chant build src/ --output dist/
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
- `--watch` — rebuild on source changes
|
|
37
|
+
- `--format json` — not applicable (Temporal outputs are YAML/shell/TypeScript)
|
|
38
|
+
|
|
39
|
+
### Lint the source
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
chant lint src/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Start the Temporal server
|
|
46
|
+
|
|
47
|
+
### Local dev (generated docker-compose.yml)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Start the dev server
|
|
51
|
+
docker compose up -d
|
|
52
|
+
|
|
53
|
+
# Verify it's running
|
|
54
|
+
temporal operator cluster health
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The dev server runs as a single container (`temporal server start-dev`). The Web UI is available at `http://localhost:8080`.
|
|
58
|
+
|
|
59
|
+
### Temporal Cloud
|
|
60
|
+
|
|
61
|
+
No server to start. Configure your worker profile in `chant.config.ts`:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import type { TemporalChantConfig } from "@intentius/chant-lexicon-temporal";
|
|
65
|
+
|
|
66
|
+
export default {
|
|
67
|
+
lexicons: ["temporal"],
|
|
68
|
+
temporal: {
|
|
69
|
+
profiles: {
|
|
70
|
+
cloud: {
|
|
71
|
+
address: "myns.a2dd6.tmprl.cloud:7233",
|
|
72
|
+
namespace: "myns.a2dd6",
|
|
73
|
+
taskQueue: "my-deploy",
|
|
74
|
+
tls: true,
|
|
75
|
+
apiKey: { env: "TEMPORAL_API_KEY" },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultProfile: "cloud",
|
|
79
|
+
} satisfies TemporalChantConfig,
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Provision namespaces and search attributes
|
|
84
|
+
|
|
85
|
+
After the server is ready, run the generated setup script:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bash dist/temporal-setup.sh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This creates namespaces and registers search attributes using the `temporal` CLI. The `TEMPORAL_ADDRESS` env var overrides the default `localhost:7233`:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
TEMPORAL_ADDRESS=myns.a2dd6.tmprl.cloud:7233 bash dist/temporal-setup.sh
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Verify:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
temporal operator namespace describe --namespace default
|
|
101
|
+
temporal operator search-attribute list --namespace default
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Deploy with Helm
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
helm repo add temporal https://go.temporal.io/server/helm-charts
|
|
108
|
+
helm repo update
|
|
109
|
+
helm install temporal temporal/temporal -f dist/temporal-helm-values.yaml
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Wait for all pods:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
kubectl get pods -l app.kubernetes.io/name=temporal -w
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Register schedules
|
|
119
|
+
|
|
120
|
+
Each `TemporalSchedule` resource generates a standalone TypeScript runner:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Set connection env vars
|
|
124
|
+
export TEMPORAL_ADDRESS=localhost:7233
|
|
125
|
+
export TEMPORAL_NAMESPACE=default
|
|
126
|
+
|
|
127
|
+
# Run the generated schedule creation script
|
|
128
|
+
npx tsx dist/schedules/daily-backup.ts
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Verify the schedule was created:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
temporal schedule list --namespace default
|
|
135
|
+
temporal schedule describe --schedule-id daily-backup --namespace default
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Key resource types
|
|
139
|
+
|
|
140
|
+
| Resource | Purpose |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `TemporalServer` | Server deployment config (dev vs full mode) |
|
|
143
|
+
| `TemporalNamespace` | Namespace with retention policy |
|
|
144
|
+
| `SearchAttribute` | Custom workflow search field |
|
|
145
|
+
| `TemporalSchedule` | Recurring workflow trigger |
|
|
146
|
+
|
|
147
|
+
## Common patterns
|
|
148
|
+
|
|
149
|
+
### Minimal local dev stack
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { TemporalServer, TemporalNamespace } from "@intentius/chant-lexicon-temporal";
|
|
153
|
+
|
|
154
|
+
export const server = new TemporalServer({ mode: "dev" });
|
|
155
|
+
export const ns = new TemporalNamespace({ name: "default", retention: "7d" });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Production namespace with search attributes
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { TemporalNamespace, SearchAttribute } from "@intentius/chant-lexicon-temporal";
|
|
162
|
+
|
|
163
|
+
export const ns = new TemporalNamespace({
|
|
164
|
+
name: "prod-deploy",
|
|
165
|
+
retention: "30d",
|
|
166
|
+
description: "Production deployment workflows",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export const gcpProject = new SearchAttribute({
|
|
170
|
+
name: "GcpProject",
|
|
171
|
+
type: "Text",
|
|
172
|
+
namespace: "prod-deploy",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export const environment = new SearchAttribute({
|
|
176
|
+
name: "Environment",
|
|
177
|
+
type: "Keyword",
|
|
178
|
+
namespace: "prod-deploy",
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Recurring backup schedule
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { TemporalSchedule } from "@intentius/chant-lexicon-temporal";
|
|
186
|
+
|
|
187
|
+
export const backupSchedule = new TemporalSchedule({
|
|
188
|
+
scheduleId: "daily-backup",
|
|
189
|
+
spec: {
|
|
190
|
+
cronExpressions: ["0 3 * * *"],
|
|
191
|
+
},
|
|
192
|
+
action: {
|
|
193
|
+
workflowType: "backupWorkflow",
|
|
194
|
+
taskQueue: "backup-queue",
|
|
195
|
+
},
|
|
196
|
+
policies: {
|
|
197
|
+
overlap: "Skip",
|
|
198
|
+
pauseOnFailure: true,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intentius/chant-lexicon-temporal",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Temporal lexicon for chant — server deployment, namespaces, search attributes, and schedules",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/",
|
|
9
|
+
"dist/"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.ts",
|
|
13
|
+
"./*": "./src/*.ts",
|
|
14
|
+
"./manifest": "./dist/manifest.json",
|
|
15
|
+
"./meta": "./dist/meta.json",
|
|
16
|
+
"./types": "./dist/types/index.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"generate": "tsx src/codegen/generate-cli.ts",
|
|
20
|
+
"bundle": "tsx src/codegen/package-cli.ts",
|
|
21
|
+
"validate": "tsx src/validate-cli.ts",
|
|
22
|
+
"docs": "tsx src/codegen/docs-cli.ts",
|
|
23
|
+
"prepack": "npm run generate && npm run bundle && npm run validate"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@intentius/chant": "^0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@intentius/chant": "*",
|
|
30
|
+
"typescript": "^5.9.3"
|
|
31
|
+
}
|
|
32
|
+
}
|