@polymorphism-tech/morph-spec 7.5.0 → 7.7.0
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/README.md +2 -2
- package/bin/morph-spec.js +18 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +12 -4
- package/framework/MORPH.md +18 -10
- package/framework/agents.json +13 -5
- package/framework/commands/morph-preflight.md +10 -5
- package/framework/hooks/README.md +8 -10
- package/framework/hooks/claude-code/core/gate-guard.js +36 -16
- package/framework/hooks/claude-code/core/protect-spec-files.js +20 -5
- package/framework/hooks/claude-code/core/tasks-json-guard.js +81 -0
- package/framework/hooks/shared/tasks-validator.js +68 -0
- package/framework/mcp/registry.json +5 -48
- package/framework/rubrics/backend.json +3 -3
- package/framework/rubrics/default.json +3 -3
- package/framework/rubrics/frontend.json +3 -3
- package/framework/rules/infrastructure-standards.md +4 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +22 -2
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +84 -5
- package/framework/skills/core/morph-apply/SKILL.md +53 -40
- package/framework/skills/core/morph-brainstorming/SKILL.md +7 -12
- package/framework/skills/core/morph-eval/SKILL.md +10 -8
- package/framework/skills/core/morph-implement/SKILL.md +27 -21
- package/framework/skills/core/morph-plan/SKILL.md +12 -21
- package/framework/skills/core/morph-proposal/SKILL.md +3 -8
- package/framework/skills/core/morph-review/SKILL.md +30 -14
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +80 -8
- package/framework/standards/ai-agents/durable-workflows-hitl.md +191 -0
- package/framework/standards/ai-agents/rag-custom-pgvector.md +246 -243
- package/framework/standards/ai-agents/setup.md +3 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +170 -0
- package/framework/standards/ai-agents/workflows.md +190 -187
- package/framework/standards/infrastructure/docker/{easypanel-deploy.md → coolify-deploy.md} +49 -20
- package/framework/standards/integration/mcp/mcp-tools.md +38 -43
- package/framework/templates/README.md +7 -7
- package/framework/templates/REGISTRY.json +14 -14
- package/framework/templates/context/CONTEXT.md +3 -3
- package/framework/templates/docs/proposal.md +2 -2
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +1 -1
- package/framework/templates/infrastructure/docker/coolify.template.json +49 -0
- package/framework/templates/infrastructure/github/README.md +6 -6
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +33 -0
- package/package.json +1 -1
- package/src/commands/project/doctor.js +36 -1
- package/src/commands/state/approve.js +29 -4
- package/src/commands/state/score.js +88 -0
- package/src/commands/tasks/dag.js +68 -0
- package/src/dashboard/scanner/rubrics-scanner.js +1 -1
- package/src/lib/generators/recap-generator.js +1 -1
- package/src/lib/tasks/dispatch-planner.js +201 -0
- package/src/lib/validators/score-aggregator.js +3 -3
- package/src/scripts/setup-infra.js +42 -0
- package/src/utils/agents-installer.js +142 -10
- package/src/utils/hooks-installer.js +13 -5
- package/framework/rubrics/eval-rubric.json +0 -66
- package/framework/templates/infrastructure/docker/easypanel.template.json +0 -46
- package/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +0 -31
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* Installs morph-spec domain personas as native Claude Code subagents
|
|
5
5
|
* in .claude/agents/morph-{id}.md.
|
|
6
6
|
*
|
|
7
|
+
* agents.json is the SINGLE SOURCE OF TRUTH (ADR-001, docs/specs/
|
|
8
|
+
* dispatchable-personas/decisions.md). The .md files are DERIVED artifacts:
|
|
9
|
+
* generated with a MANAGED header, regenerated on update/--reinstall, and
|
|
10
|
+
* drift-checked by `morph-spec doctor` (checkAgentsDrift). Never hand-edit
|
|
11
|
+
* them — that is exactly the v4 dual-source drift the v5 migration removed.
|
|
12
|
+
*
|
|
13
|
+
* Canonical naming: subagent_type = `morph-{id}` — the frontmatter `name`
|
|
14
|
+
* matches the filename 1:1, and the skills dispatch that exact type.
|
|
15
|
+
*
|
|
7
16
|
* In v5 the harness has 7 domain personas (no tiers) — each persona is a
|
|
8
17
|
* subagent the Specialist (Claude in implementer role) can dispatch when
|
|
9
18
|
* working in that domain. The 3 roles (Orchestrator/Specialist/Evaluator)
|
|
@@ -14,6 +23,33 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlink
|
|
|
14
23
|
import { join } from 'path';
|
|
15
24
|
import { shouldInstall } from '../lib/stack-filter.js';
|
|
16
25
|
|
|
26
|
+
/** Default tool scope for domain personas (override per agent via agents.json `tools`). */
|
|
27
|
+
export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load the ids of MCP servers known to the framework registry
|
|
31
|
+
* (framework/mcp/registry.json). Used to validate agents.json `mcp` entries:
|
|
32
|
+
* only registry-known servers become tool grants (fail-open on any error).
|
|
33
|
+
*
|
|
34
|
+
* @param {string} frameworkDir
|
|
35
|
+
* @returns {Set<string>|null} null = no registry found (accept nothing)
|
|
36
|
+
*/
|
|
37
|
+
export function loadKnownMcpIds(frameworkDir) {
|
|
38
|
+
try {
|
|
39
|
+
const registryPath = join(frameworkDir, 'mcp', 'registry.json');
|
|
40
|
+
if (!existsSync(registryPath)) return null;
|
|
41
|
+
const registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
|
|
42
|
+
return new Set(Object.keys(registry.mcps ?? {}));
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const MANAGED_HEADER =
|
|
49
|
+
'<!-- MANAGED BY morph-spec — generated from .morph/framework/agents.json (single source of truth).\n' +
|
|
50
|
+
' Do NOT edit by hand: changes are overwritten on `morph-spec update` / `setup-infra --reinstall`.\n' +
|
|
51
|
+
' To change this persona, edit agents.json and regenerate. Drift is flagged by `morph-spec doctor`. -->';
|
|
52
|
+
|
|
17
53
|
/**
|
|
18
54
|
* Install morph-spec domain personas as native Claude Code subagents.
|
|
19
55
|
*
|
|
@@ -37,6 +73,8 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
37
73
|
const targetDir = join(projectDir, '.claude', 'agents');
|
|
38
74
|
mkdirSync(targetDir, { recursive: true });
|
|
39
75
|
|
|
76
|
+
const knownMcpIds = loadKnownMcpIds(frameworkDir);
|
|
77
|
+
|
|
40
78
|
// Filter by stack
|
|
41
79
|
const eligible = agents.filter(a => {
|
|
42
80
|
const stack = a.stack ?? ['*'];
|
|
@@ -48,17 +86,20 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
48
86
|
|
|
49
87
|
let installed = 0;
|
|
50
88
|
const installedFilenames = new Set();
|
|
89
|
+
const installedAgents = [];
|
|
51
90
|
|
|
52
91
|
for (const agent of eligible) {
|
|
53
92
|
const filename = `morph-${agent.id}.md`;
|
|
54
93
|
const targetPath = join(targetDir, filename);
|
|
55
94
|
|
|
56
|
-
|
|
57
|
-
const body = buildBody(agent);
|
|
58
|
-
const frontmatter = buildFrontmatter(agent, description);
|
|
59
|
-
|
|
60
|
-
writeFileSync(targetPath, `---\n${frontmatter}---\n\n${body}\n`, 'utf-8');
|
|
95
|
+
writeFileSync(targetPath, renderAgentMd(agent, knownMcpIds), 'utf-8');
|
|
61
96
|
installedFilenames.add(filename);
|
|
97
|
+
installedAgents.push({
|
|
98
|
+
id: agent.id,
|
|
99
|
+
name: `morph-${agent.id}`,
|
|
100
|
+
title: agent.title ?? agent.id,
|
|
101
|
+
domains: agent.domains ?? [],
|
|
102
|
+
});
|
|
62
103
|
installed++;
|
|
63
104
|
}
|
|
64
105
|
|
|
@@ -69,15 +110,46 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
69
110
|
}
|
|
70
111
|
}
|
|
71
112
|
|
|
72
|
-
return { installed };
|
|
113
|
+
return { installed, agents: installedAgents };
|
|
73
114
|
}
|
|
74
115
|
|
|
75
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Render the full .md content for a persona — deterministic, so the same
|
|
118
|
+
* function serves the installer AND the doctor drift check (a hand-edited or
|
|
119
|
+
* stale file differs byte-for-byte from this output).
|
|
120
|
+
*
|
|
121
|
+
* @param {object} agent - agents.json entry with `id` merged in
|
|
122
|
+
* @param {Set<string>|null} [knownMcpIds] - registry-known MCP ids; agents.json
|
|
123
|
+
* `mcp` entries outside this set are ignored (null = ignore all)
|
|
124
|
+
* @returns {string} complete file content
|
|
125
|
+
*/
|
|
126
|
+
export function renderAgentMd(agent, knownMcpIds = null) {
|
|
127
|
+
const description = buildDescription(agent);
|
|
128
|
+
const frontmatter = buildFrontmatter(agent, description, knownMcpIds);
|
|
129
|
+
const body = buildBody(agent);
|
|
130
|
+
return `---\n${frontmatter}---\n\n${MANAGED_HEADER}\n\n${body}\n`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildToolsLine(agent, knownMcpIds) {
|
|
134
|
+
const base = agent.tools ?? DEFAULT_AGENT_TOOLS;
|
|
135
|
+
// Domain-relevant MCP servers (agents.json `mcp: [ids]`) become server-level
|
|
136
|
+
// grants (`mcp__{id}`). Only registry-known ids are emitted; in projects
|
|
137
|
+
// without that server configured the grant is inert.
|
|
138
|
+
const mcpGrants = (agent.mcp ?? [])
|
|
139
|
+
.filter(id => knownMcpIds?.has(id))
|
|
140
|
+
.map(id => `mcp__${id}`);
|
|
141
|
+
return [base, ...mcpGrants].join(', ');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildFrontmatter(agent, description, knownMcpIds) {
|
|
76
145
|
const lines = [
|
|
77
|
-
|
|
146
|
+
// Canonical 1:1 naming: frontmatter name === filename === subagent_type.
|
|
147
|
+
`name: morph-${agent.id}`,
|
|
78
148
|
`description: ${description}`,
|
|
79
|
-
|
|
80
|
-
|
|
149
|
+
// Domain implementers inherit the session model by default — a hardcoded
|
|
150
|
+
// downgrade here would silently lower quality. Override via agents.json.
|
|
151
|
+
`model: ${agent.model ?? 'inherit'}`,
|
|
152
|
+
`tools: ${buildToolsLine(agent, knownMcpIds)}`,
|
|
81
153
|
`maxTurns: 20`,
|
|
82
154
|
];
|
|
83
155
|
if (agent.color) lines.push(`color: ${agent.color}`);
|
|
@@ -171,6 +243,66 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
|
171
243
|
return { missing, installed: installedFiles.size, expected };
|
|
172
244
|
}
|
|
173
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Drift check between agents.json (source of truth) and the generated
|
|
248
|
+
* .claude/agents/morph-*.md files. Consumed by `morph-spec doctor`.
|
|
249
|
+
*
|
|
250
|
+
* - missing: eligible persona with no .md installed → run setup-infra --reinstall
|
|
251
|
+
* - stale: .md exists but differs from renderAgentMd() output (hand-edited
|
|
252
|
+
* or generated by an older CLI) → regenerate
|
|
253
|
+
* - orphans: morph-*.md files with no matching agents.json entry
|
|
254
|
+
*
|
|
255
|
+
* @param {string} projectDir
|
|
256
|
+
* @param {string} frameworkDir - directory containing agents.json
|
|
257
|
+
* @param {string[]} projectTags
|
|
258
|
+
* @returns {{ missing: string[], stale: string[], orphans: string[], expected: number }}
|
|
259
|
+
*/
|
|
260
|
+
export function checkAgentsDrift(projectDir, frameworkDir, projectTags = []) {
|
|
261
|
+
const targetDir = join(projectDir, '.claude', 'agents');
|
|
262
|
+
const agentsJsonPath = join(frameworkDir, 'agents.json');
|
|
263
|
+
if (!existsSync(agentsJsonPath)) {
|
|
264
|
+
return { missing: [], stale: [], orphans: [], expected: 0 };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
|
|
268
|
+
const agents = Object.entries(agentsJson.agents ?? {})
|
|
269
|
+
.filter(([key]) => !key.startsWith('_comment'))
|
|
270
|
+
.map(([id, data]) => ({ id, ...data }));
|
|
271
|
+
|
|
272
|
+
const knownMcpIds = loadKnownMcpIds(frameworkDir);
|
|
273
|
+
const knownFilenames = new Set(agents.map(a => `morph-${a.id}.md`));
|
|
274
|
+
const missing = [];
|
|
275
|
+
const stale = [];
|
|
276
|
+
let expected = 0;
|
|
277
|
+
|
|
278
|
+
for (const agent of agents) {
|
|
279
|
+
const stack = agent.stack ?? ['*'];
|
|
280
|
+
if (!shouldInstall(stack, projectTags)) continue;
|
|
281
|
+
expected++;
|
|
282
|
+
const filename = `morph-${agent.id}.md`;
|
|
283
|
+
const filePath = join(targetDir, filename);
|
|
284
|
+
if (!existsSync(filePath)) {
|
|
285
|
+
missing.push(filename);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const onDisk = readFileSync(filePath, 'utf-8').replace(/\r\n/g, '\n');
|
|
289
|
+
if (onDisk !== renderAgentMd(agent, knownMcpIds)) {
|
|
290
|
+
stale.push(filename);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const orphans = [];
|
|
295
|
+
if (existsSync(targetDir)) {
|
|
296
|
+
for (const file of readdirSync(targetDir)) {
|
|
297
|
+
if (file.startsWith('morph-') && file.endsWith('.md') && !knownFilenames.has(file)) {
|
|
298
|
+
orphans.push(file);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { missing, stale, orphans, expected };
|
|
304
|
+
}
|
|
305
|
+
|
|
174
306
|
/**
|
|
175
307
|
* Compatibility export — kept so existing tests/CLI code that imports
|
|
176
308
|
* formatStandardRef or parseSkillFile / collectSkillFiles still work,
|
|
@@ -102,14 +102,22 @@ const MORPH_HOOKS = [
|
|
|
102
102
|
]
|
|
103
103
|
},
|
|
104
104
|
|
|
105
|
-
// === PostToolUse: Write|Edit — typecheck (debounced, silent on success)
|
|
105
|
+
// === PostToolUse: Write|Edit — typecheck (debounced, silent on success) +
|
|
106
|
+
// tasks.json schema guard (validate-and-warn; status enum, effort enum) ===
|
|
106
107
|
{
|
|
107
108
|
event: 'PostToolUse',
|
|
108
109
|
matcher: 'Write|Edit',
|
|
109
|
-
hooks: [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
hooks: [
|
|
111
|
+
{
|
|
112
|
+
type: 'command',
|
|
113
|
+
command: 'node framework/hooks/claude-code/core/post-edit-typecheck.js'
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: 'command',
|
|
117
|
+
command: 'node framework/hooks/claude-code/core/tasks-json-guard.js',
|
|
118
|
+
timeout: 3000
|
|
119
|
+
}
|
|
120
|
+
]
|
|
113
121
|
},
|
|
114
122
|
|
|
115
123
|
// === PostToolUse: Write|Edit|AskUserQuestion — state-sync (autonomous state.json updates) ===
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "eval",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"description": "Eval rubric for morph-eval system. Scores phase outputs across structural and semantic dimensions.",
|
|
5
|
-
"categories": {
|
|
6
|
-
"structural_compliance": {
|
|
7
|
-
"weight": 0.25,
|
|
8
|
-
"description": "Required sections, headers, formatting present in phase outputs",
|
|
9
|
-
"dimensions": {
|
|
10
|
-
"proposal": ["has_problem_statement", "has_proposed_solution", "min_length_300"],
|
|
11
|
-
"spec": ["has_overview", "has_functional_requirements", "has_fr_sections", "has_data_model"],
|
|
12
|
-
"tasks": ["has_task_headers", "has_fr_references", "no_duplicate_ids"],
|
|
13
|
-
"contracts": ["has_type_definitions", "correct_naming_conventions"]
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"content_quality": {
|
|
17
|
-
"weight": 0.30,
|
|
18
|
-
"description": "LLM-as-judge: completeness, specificity, actionability of outputs",
|
|
19
|
-
"dimensions": {
|
|
20
|
-
"completeness": "Covers all aspects of the feature request",
|
|
21
|
-
"specificity": "Requirements are concrete and testable, not vague",
|
|
22
|
-
"actionability": "A developer could implement from this alone",
|
|
23
|
-
"architecture_depth": "Component boundaries, data flow, error handling clear"
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"cross_phase_consistency": {
|
|
27
|
-
"weight": 0.25,
|
|
28
|
-
"description": "Alignment between spec, tasks, mandate, and contracts",
|
|
29
|
-
"dimensions": {
|
|
30
|
-
"fr_coverage": "Every spec FR referenced by at least one task",
|
|
31
|
-
"mandate_integrity": "Mandate includes all tasks with acceptance criteria",
|
|
32
|
-
"contract_alignment": "Spec Data Model entities appear in contracts",
|
|
33
|
-
"dependency_ordering": "Task dependencies are topologically valid"
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"stack_appropriateness": {
|
|
37
|
-
"weight": 0.20,
|
|
38
|
-
"description": "Outputs match the target technology stack",
|
|
39
|
-
"dimensions": {
|
|
40
|
-
"correct_language": "Contracts use the right language for the stack (TS vs CS)",
|
|
41
|
-
"framework_patterns": "Architecture follows stack conventions (App Router, VSA, etc.)",
|
|
42
|
-
"tool_chain": "Build/test commands match the stack"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"thresholds": {
|
|
47
|
-
"approve": 8.0,
|
|
48
|
-
"retry": 6.0,
|
|
49
|
-
"max_iterations": 3,
|
|
50
|
-
"regression_tolerance": 0.5
|
|
51
|
-
},
|
|
52
|
-
"scoring": {
|
|
53
|
-
"structural": {
|
|
54
|
-
"method": "binary_checklist",
|
|
55
|
-
"description": "Each dimension is pass/fail. Score = (passed / total) * 10"
|
|
56
|
-
},
|
|
57
|
-
"llm_judge": {
|
|
58
|
-
"method": "evidence_based",
|
|
59
|
-
"description": "LLM scores 1-10 per dimension with evidence quotes and gap descriptions"
|
|
60
|
-
},
|
|
61
|
-
"cross_phase": {
|
|
62
|
-
"method": "ratio_based",
|
|
63
|
-
"description": "FR coverage ratio, mandate inclusion ratio. Score = ratio * 10"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "{{kebabCase FEATURE_NAME}}",
|
|
3
|
-
"services": [
|
|
4
|
-
{
|
|
5
|
-
"type": "app",
|
|
6
|
-
"data": {
|
|
7
|
-
"projectName": "{{kebabCase FEATURE_NAME}}",
|
|
8
|
-
"serviceName": "{{kebabCase FEATURE_NAME}}-app",
|
|
9
|
-
"source": {
|
|
10
|
-
"type": "github",
|
|
11
|
-
"owner": "{{GITHUB_OWNER}}",
|
|
12
|
-
"repo": "{{GITHUB_REPO}}",
|
|
13
|
-
"ref": "main",
|
|
14
|
-
"autoDeploy": true
|
|
15
|
-
},
|
|
16
|
-
"build": {
|
|
17
|
-
"type": "dockerfile",
|
|
18
|
-
"dockerfile": "Dockerfile"
|
|
19
|
-
},
|
|
20
|
-
"deploy": {
|
|
21
|
-
"replicas": 1,
|
|
22
|
-
"command": null,
|
|
23
|
-
"zeroDowntime": true
|
|
24
|
-
},
|
|
25
|
-
"env": [
|
|
26
|
-
{
|
|
27
|
-
"key": "NODE_ENV",
|
|
28
|
-
"value": "production"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"key": "DATABASE_URL",
|
|
32
|
-
"value": "${DATABASE_URL}"
|
|
33
|
-
}
|
|
34
|
-
],
|
|
35
|
-
"domains": [
|
|
36
|
-
{
|
|
37
|
-
"host": "{{kebabCase FEATURE_NAME}}.easypanel.host",
|
|
38
|
-
"port": 3000,
|
|
39
|
-
"https": true
|
|
40
|
-
}
|
|
41
|
-
],
|
|
42
|
-
"mounts": []
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
]
|
|
46
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
name: Deploy to EasyPanel
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_call:
|
|
5
|
-
inputs:
|
|
6
|
-
environment:
|
|
7
|
-
required: true
|
|
8
|
-
type: string
|
|
9
|
-
image:
|
|
10
|
-
required: true
|
|
11
|
-
type: string
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
deploy:
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
environment: ${{{{ inputs.environment }}}}
|
|
17
|
-
steps:
|
|
18
|
-
- name: Trigger EasyPanel Deploy
|
|
19
|
-
run: |
|
|
20
|
-
curl -X POST ${{{{ secrets.EASYPANEL_WEBHOOK_URL }}}} \
|
|
21
|
-
-H "Authorization: Bearer ${{{{ secrets.EASYPANEL_API_TOKEN }}}}" \
|
|
22
|
-
-H "Content-Type: application/json" \
|
|
23
|
-
-d '{
|
|
24
|
-
"image": "${{{{ inputs.image }}}}",
|
|
25
|
-
"environment": "${{{{ inputs.environment }}}}"
|
|
26
|
-
}'
|
|
27
|
-
|
|
28
|
-
- name: Health Check
|
|
29
|
-
run: |
|
|
30
|
-
sleep 30 # Wait for deployment
|
|
31
|
-
curl --retry 10 --retry-delay 5 --retry-connrefused ${{{{ secrets.APP_URL }}}}/health
|