@mgsoftwarebv/mg-dashboard-mcp 7.0.8 → 7.0.10
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/index.js +189 -936
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,8 +21,7 @@ import { once } from 'events';
|
|
|
21
21
|
import { lookup } from 'dns/promises';
|
|
22
22
|
import { connect } from 'tls';
|
|
23
23
|
import { pgTable, timestamp, jsonb, uuid, boolean, text, pgEnum, integer, uniqueIndex, index, bigint } from 'drizzle-orm/pg-core';
|
|
24
|
-
import {
|
|
25
|
-
import { HeadObjectCommand, S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
|
|
24
|
+
import { HeadObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, S3Client, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
|
|
26
25
|
|
|
27
26
|
var __defProp = Object.defineProperty;
|
|
28
27
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -684,10 +683,10 @@ var TRIGGER_TOOL_MODULE_MAP = {
|
|
|
684
683
|
"trigger-run": "ci_cd"
|
|
685
684
|
};
|
|
686
685
|
async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
|
|
687
|
-
const
|
|
686
|
+
const sql23 = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
|
|
688
687
|
const cmd = [
|
|
689
688
|
`PORT=$(docker port "${WA_CONTAINER}" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
|
|
690
|
-
`KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${
|
|
689
|
+
`KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql23}" 2>/dev/null | tr -d '[:space:]')`,
|
|
691
690
|
'echo "$PORT|$KEY"'
|
|
692
691
|
].join(" && ");
|
|
693
692
|
const result = await sshExec2(conn, cmd, proxy);
|
|
@@ -708,8 +707,8 @@ async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
|
|
|
708
707
|
return { port, apiKey: apiKey2 };
|
|
709
708
|
}
|
|
710
709
|
async function fetchRunLogs(runId, conn, proxy, sshExec2) {
|
|
711
|
-
const
|
|
712
|
-
const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${
|
|
710
|
+
const sql23 = `SELECT level, message, \\"isError\\", \\"createdAt\\" FROM \\"TaskEvent\\" WHERE \\"runId\\" = '${runId}' AND level IN ('INFO','WARN','ERROR','DEBUG','LOG','TRACE') ORDER BY \\"startTime\\" ASC LIMIT 200`;
|
|
711
|
+
const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql23}" 2>/dev/null`;
|
|
713
712
|
const result = await sshExec2(conn, cmd, proxy);
|
|
714
713
|
const output = result.stdout.trim();
|
|
715
714
|
if (!output) return "";
|
|
@@ -791,8 +790,8 @@ async function handleTriggerTool(name, args2, deps) {
|
|
|
791
790
|
switch (name) {
|
|
792
791
|
// -----------------------------------------------------------------
|
|
793
792
|
case "trigger-list": {
|
|
794
|
-
const
|
|
795
|
-
const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${
|
|
793
|
+
const sql23 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
|
|
794
|
+
const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql23}" 2>/dev/null`;
|
|
796
795
|
const result = await sshExec2(conn, cmd, proxy);
|
|
797
796
|
const output = result.stdout.trim();
|
|
798
797
|
if (!output) {
|
|
@@ -5746,563 +5745,7 @@ pgTable(
|
|
|
5746
5745
|
index("github_webhook_secret_owner_idx").on(table.owner)
|
|
5747
5746
|
]
|
|
5748
5747
|
);
|
|
5749
|
-
function assertPipelinePayload(payload) {
|
|
5750
|
-
if (!payload.stepIds?.length) {
|
|
5751
|
-
throw new Error("Cannot trigger execute-pipeline: stepIds is empty");
|
|
5752
|
-
}
|
|
5753
|
-
}
|
|
5754
|
-
async function triggerPipeline(payload) {
|
|
5755
|
-
assertPipelinePayload(payload);
|
|
5756
|
-
return tasks.trigger(
|
|
5757
|
-
"execute-pipeline",
|
|
5758
|
-
payload
|
|
5759
|
-
);
|
|
5760
|
-
}
|
|
5761
|
-
|
|
5762
|
-
// ../jobs/src/github-changed-files.ts
|
|
5763
5748
|
var GITHUB_API = "https://api.github.com";
|
|
5764
|
-
async function fetchCompareChangedPaths(repoFullName, baseSha, headSha, token) {
|
|
5765
|
-
if (baseSha === headSha) return [];
|
|
5766
|
-
const res = await fetch(
|
|
5767
|
-
`${GITHUB_API}/repos/${repoFullName}/compare/${baseSha}...${headSha}`,
|
|
5768
|
-
{
|
|
5769
|
-
headers: {
|
|
5770
|
-
Accept: "application/vnd.github.v3+json",
|
|
5771
|
-
Authorization: `Bearer ${token}`
|
|
5772
|
-
}
|
|
5773
|
-
}
|
|
5774
|
-
);
|
|
5775
|
-
if (!res.ok) {
|
|
5776
|
-
const body = await res.text().catch(() => "");
|
|
5777
|
-
throw new Error(
|
|
5778
|
-
`GitHub compare ${baseSha.slice(0, 8)}...${headSha.slice(0, 8)} failed (${res.status}): ${body.slice(0, 200)}`
|
|
5779
|
-
);
|
|
5780
|
-
}
|
|
5781
|
-
const detail = await res.json();
|
|
5782
|
-
return (detail.files ?? []).map((file) => file.filename).filter((name) => Boolean(name));
|
|
5783
|
-
}
|
|
5784
|
-
function globToRegex(pattern) {
|
|
5785
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*");
|
|
5786
|
-
return new RegExp(`^${escaped}$`);
|
|
5787
|
-
}
|
|
5788
|
-
function matchesPathPatterns(changedPaths, patterns) {
|
|
5789
|
-
if (patterns.length === 0) return false;
|
|
5790
|
-
const regexes = patterns.map(globToRegex);
|
|
5791
|
-
return changedPaths.some((path) => regexes.some((regex) => regex.test(path)));
|
|
5792
|
-
}
|
|
5793
|
-
function parseScriptIdList(value) {
|
|
5794
|
-
if (!Array.isArray(value)) return [];
|
|
5795
|
-
return value.filter((item) => typeof item === "string");
|
|
5796
|
-
}
|
|
5797
|
-
async function fetchDeployRetryScripts(stageId) {
|
|
5798
|
-
const rows = await getDb().execute(
|
|
5799
|
-
sql`SELECT deploy_retry_scripts FROM release_profile_stage WHERE id = ${stageId} LIMIT 1`
|
|
5800
|
-
);
|
|
5801
|
-
return parseScriptIdList(rows[0]?.deploy_retry_scripts);
|
|
5802
|
-
}
|
|
5803
|
-
async function fetchLastSuccessfulScriptDeploySha(stageId, scriptId) {
|
|
5804
|
-
const rows = await getDb().execute(
|
|
5805
|
-
sql`SELECT r.trigger_sha
|
|
5806
|
-
FROM release_step rs
|
|
5807
|
-
JOIN release r ON r.id = rs.release_id
|
|
5808
|
-
WHERE r.release_profile_stage_id = ${stageId}
|
|
5809
|
-
AND rs.step_type = 'run_ssh_script'
|
|
5810
|
-
AND rs.step_config->>'scriptId' = ${scriptId}
|
|
5811
|
-
AND rs.status = 'success'
|
|
5812
|
-
AND r.trigger_sha IS NOT NULL
|
|
5813
|
-
ORDER BY rs.finished_at DESC NULLS LAST
|
|
5814
|
-
LIMIT 1`
|
|
5815
|
-
);
|
|
5816
|
-
return rows[0]?.trigger_sha ?? null;
|
|
5817
|
-
}
|
|
5818
|
-
|
|
5819
|
-
// ../jobs/src/release-pipeline-config.ts
|
|
5820
|
-
function shouldRunSshScript(script, context) {
|
|
5821
|
-
if (script.always) return true;
|
|
5822
|
-
if (!script.paths?.length) return false;
|
|
5823
|
-
if (context.runAllConditional) return true;
|
|
5824
|
-
if (context.latchedScriptIds?.includes(script.id)) return true;
|
|
5825
|
-
const comparePaths = context.compareChangedPathsByScriptId?.[script.id];
|
|
5826
|
-
if (comparePaths === null) return true;
|
|
5827
|
-
if (comparePaths !== void 0) {
|
|
5828
|
-
if (!comparePaths.length) return false;
|
|
5829
|
-
return matchesPathPatterns(comparePaths, script.paths);
|
|
5830
|
-
}
|
|
5831
|
-
if (!context.changedPaths?.length) return false;
|
|
5832
|
-
return matchesPathPatterns(context.changedPaths, script.paths);
|
|
5833
|
-
}
|
|
5834
|
-
function buildSkipReason(script) {
|
|
5835
|
-
const patterns = script.paths?.join(", ") ?? "configured paths";
|
|
5836
|
-
return `Skipped: no matching file changes (${patterns})`;
|
|
5837
|
-
}
|
|
5838
|
-
function parseReleasePipelineConfig(value) {
|
|
5839
|
-
if (!value || typeof value !== "object") return null;
|
|
5840
|
-
const config = value;
|
|
5841
|
-
const hasSshScripts = Array.isArray(config.sshScripts) && config.sshScripts.length > 0;
|
|
5842
|
-
const hasMigration = config.migration != null && typeof config.migration === "object";
|
|
5843
|
-
const hasNpmPublish = Array.isArray(config.npmPublish) && config.npmPublish.length > 0;
|
|
5844
|
-
if (!hasSshScripts && !hasMigration && !hasNpmPublish) return null;
|
|
5845
|
-
return config;
|
|
5846
|
-
}
|
|
5847
|
-
function injectSshScriptSteps(baseSteps, config, context = {}) {
|
|
5848
|
-
const scripts = config?.sshScripts;
|
|
5849
|
-
if (!scripts?.length) return baseSteps;
|
|
5850
|
-
const result = [];
|
|
5851
|
-
for (const step of baseSteps) {
|
|
5852
|
-
result.push(step);
|
|
5853
|
-
for (const script of scripts) {
|
|
5854
|
-
if (script.after !== step.type) continue;
|
|
5855
|
-
const run = shouldRunSshScript(script, context);
|
|
5856
|
-
result.push({
|
|
5857
|
-
type: "run_ssh_script",
|
|
5858
|
-
label: script.label,
|
|
5859
|
-
stepConfig: {
|
|
5860
|
-
scriptId: script.id,
|
|
5861
|
-
script: script.script
|
|
5862
|
-
},
|
|
5863
|
-
initialStatus: run ? "pending" : "skipped",
|
|
5864
|
-
skipReason: run ? void 0 : buildSkipReason(script)
|
|
5865
|
-
});
|
|
5866
|
-
}
|
|
5867
|
-
}
|
|
5868
|
-
return result;
|
|
5869
|
-
}
|
|
5870
|
-
function parseDeployRetryScripts(value) {
|
|
5871
|
-
if (!Array.isArray(value)) return [];
|
|
5872
|
-
return value.filter((item) => typeof item === "string");
|
|
5873
|
-
}
|
|
5874
|
-
async function resolveBuildStepListOptions(args2) {
|
|
5875
|
-
const options = {};
|
|
5876
|
-
const pipelineConfig = parseReleasePipelineConfig(
|
|
5877
|
-
args2.profile.release_pipeline_config
|
|
5878
|
-
);
|
|
5879
|
-
if (!pipelineConfig?.sshScripts?.length) {
|
|
5880
|
-
return options;
|
|
5881
|
-
}
|
|
5882
|
-
const latchedFromStage = args2.stageId ? await fetchDeployRetryScripts(args2.stageId) : parseDeployRetryScripts(args2.deployRetryScripts);
|
|
5883
|
-
options.latchedScriptIds = latchedFromStage;
|
|
5884
|
-
if (args2.isManual) {
|
|
5885
|
-
options.runAllConditional = true;
|
|
5886
|
-
return options;
|
|
5887
|
-
}
|
|
5888
|
-
if (!args2.triggerSha || !args2.profile.repo_full_name || !args2.githubToken) {
|
|
5889
|
-
return options;
|
|
5890
|
-
}
|
|
5891
|
-
const compareChangedPathsByScriptId = {};
|
|
5892
|
-
const conditionalScripts = pipelineConfig.sshScripts.filter(
|
|
5893
|
-
(script) => !script.always && script.paths?.length
|
|
5894
|
-
);
|
|
5895
|
-
await Promise.all(
|
|
5896
|
-
conditionalScripts.map(async (script) => {
|
|
5897
|
-
if (latchedFromStage.includes(script.id)) return;
|
|
5898
|
-
const stageId = args2.stageId;
|
|
5899
|
-
const lastSuccessSha = stageId != null ? await fetchLastSuccessfulScriptDeploySha(stageId, script.id) : null;
|
|
5900
|
-
if (!lastSuccessSha) {
|
|
5901
|
-
compareChangedPathsByScriptId[script.id] = null;
|
|
5902
|
-
return;
|
|
5903
|
-
}
|
|
5904
|
-
try {
|
|
5905
|
-
compareChangedPathsByScriptId[script.id] = await fetchCompareChangedPaths(
|
|
5906
|
-
args2.profile.repo_full_name,
|
|
5907
|
-
lastSuccessSha,
|
|
5908
|
-
args2.triggerSha,
|
|
5909
|
-
args2.githubToken
|
|
5910
|
-
);
|
|
5911
|
-
} catch (err) {
|
|
5912
|
-
console.warn(
|
|
5913
|
-
`[release] compare failed for script ${script.id} (${args2.profile.repo_full_name}@${args2.triggerSha?.slice(0, 8)}):`,
|
|
5914
|
-
err
|
|
5915
|
-
);
|
|
5916
|
-
compareChangedPathsByScriptId[script.id] = [];
|
|
5917
|
-
}
|
|
5918
|
-
})
|
|
5919
|
-
);
|
|
5920
|
-
options.compareChangedPathsByScriptId = compareChangedPathsByScriptId;
|
|
5921
|
-
return options;
|
|
5922
|
-
}
|
|
5923
|
-
|
|
5924
|
-
// ../jobs/src/pm2-deploy-script.ts
|
|
5925
|
-
var MG_DASHBOARD_REPO = "MGSoftwareBV/mg-dashboard";
|
|
5926
|
-
var MG_BOILER_REPO = "MGSoftwareBV/mg-boiler";
|
|
5927
|
-
function isBoilerPm2Build(buildFilters) {
|
|
5928
|
-
return buildFilters.includes("@mgboiler/");
|
|
5929
|
-
}
|
|
5930
|
-
function shouldRunConventionTests(profile, ctx) {
|
|
5931
|
-
const repo = profile.repo_full_name;
|
|
5932
|
-
if (repo === MG_DASHBOARD_REPO || repo === MG_BOILER_REPO) return true;
|
|
5933
|
-
return isBoilerPm2Build(ctx.buildFilters);
|
|
5934
|
-
}
|
|
5935
|
-
function npmPublishWatchPaths(config) {
|
|
5936
|
-
if (config.paths?.length) return config.paths;
|
|
5937
|
-
const dir = config.path.replace(/\/+$/, "");
|
|
5938
|
-
return [`${dir}/**`];
|
|
5939
|
-
}
|
|
5940
|
-
|
|
5941
|
-
// ../jobs/src/build-step-list.ts
|
|
5942
|
-
function buildStepList(stage, profile, options) {
|
|
5943
|
-
const steps = [];
|
|
5944
|
-
const dockerApps = (stage.stage_apps ?? []).filter(
|
|
5945
|
-
(a) => a.enabled && a.deployMethod === "docker"
|
|
5946
|
-
);
|
|
5947
|
-
for (const _app of dockerApps) {
|
|
5948
|
-
steps.push({ type: "deploy", label: "Deploy (Docker)" });
|
|
5949
|
-
}
|
|
5950
|
-
const pm2Apps = (stage.stage_apps ?? []).filter(
|
|
5951
|
-
(a) => a.enabled && a.deployMethod === "pm2"
|
|
5952
|
-
);
|
|
5953
|
-
const pm2Servers = new Set(
|
|
5954
|
-
pm2Apps.map((a) => a.deploymentServerId).filter(Boolean)
|
|
5955
|
-
);
|
|
5956
|
-
const boilerFilters = pm2Apps.map((a) => a.path ?? "").map((p) => p.match(/apps\/(backoffice|portal|web)/)?.[0]).filter((m) => Boolean(m)).map((m) => `--filter=@mgboiler/${m.replace("apps/", "")}`).join(" ");
|
|
5957
|
-
const runConventions = profile?.repo_full_name != null && shouldRunConventionTests(
|
|
5958
|
-
{ repo_full_name: profile.repo_full_name },
|
|
5959
|
-
{ buildFilters: boilerFilters }
|
|
5960
|
-
);
|
|
5961
|
-
if (runConventions) {
|
|
5962
|
-
for (const _server of pm2Servers) {
|
|
5963
|
-
steps.push({ type: "run_conventions", label: "Convention tests" });
|
|
5964
|
-
}
|
|
5965
|
-
}
|
|
5966
|
-
for (const _server of pm2Servers) {
|
|
5967
|
-
steps.push({ type: "build_pm2", label: "Build (PM2)" });
|
|
5968
|
-
}
|
|
5969
|
-
if (stage.migrate_db) {
|
|
5970
|
-
steps.push({ type: "migrate_db", label: "Apply Migrations" });
|
|
5971
|
-
}
|
|
5972
|
-
if (profile?.has_trigger_dev) {
|
|
5973
|
-
steps.push({ type: "deploy_trigger_jobs" });
|
|
5974
|
-
}
|
|
5975
|
-
for (const _server of pm2Servers) {
|
|
5976
|
-
steps.push({ type: "reload_pm2", label: "Reload PM2" });
|
|
5977
|
-
}
|
|
5978
|
-
const pipelineConfig = parseReleasePipelineConfig(
|
|
5979
|
-
profile?.release_pipeline_config
|
|
5980
|
-
);
|
|
5981
|
-
for (const pkg of pipelineConfig?.npmPublish ?? []) {
|
|
5982
|
-
if (!pkg.id || !pkg.path) continue;
|
|
5983
|
-
steps.push({
|
|
5984
|
-
type: "publish_npm",
|
|
5985
|
-
label: pkg.label ?? `Publish npm (${pkg.path})`,
|
|
5986
|
-
stepConfig: {
|
|
5987
|
-
packageId: pkg.id,
|
|
5988
|
-
packagePath: pkg.path,
|
|
5989
|
-
watchPaths: npmPublishWatchPaths(pkg),
|
|
5990
|
-
tokenEnvVar: pkg.tokenEnvVar
|
|
5991
|
-
}
|
|
5992
|
-
});
|
|
5993
|
-
}
|
|
5994
|
-
const withScripts = injectSshScriptSteps(steps, pipelineConfig, {
|
|
5995
|
-
changedPaths: options?.changedPaths,
|
|
5996
|
-
runAllConditional: options?.runAllConditional,
|
|
5997
|
-
latchedScriptIds: options?.latchedScriptIds,
|
|
5998
|
-
compareChangedPathsByScriptId: options?.compareChangedPathsByScriptId
|
|
5999
|
-
});
|
|
6000
|
-
if (stage.stage === "prod") {
|
|
6001
|
-
withScripts.push({
|
|
6002
|
-
type: "update_release_branch",
|
|
6003
|
-
label: "Fast-forward main"
|
|
6004
|
-
});
|
|
6005
|
-
}
|
|
6006
|
-
return withScripts;
|
|
6007
|
-
}
|
|
6008
|
-
async function insertReleaseSteps(releaseId, steps) {
|
|
6009
|
-
const db2 = getDb();
|
|
6010
|
-
const stepIds = [];
|
|
6011
|
-
for (let i = 0; i < steps.length; i++) {
|
|
6012
|
-
const step = steps[i];
|
|
6013
|
-
const status = step.initialStatus ?? "pending";
|
|
6014
|
-
const logs = status === "skipped" && step.skipReason ? step.skipReason : null;
|
|
6015
|
-
const stepConfigJson = step.stepConfig ? JSON.stringify(step.stepConfig) : null;
|
|
6016
|
-
const stepRows = await db2.execute(
|
|
6017
|
-
stepConfigJson ? sql`INSERT INTO release_step (
|
|
6018
|
-
release_id, step_type, step_order, status, step_label, step_config, logs
|
|
6019
|
-
) VALUES (
|
|
6020
|
-
${releaseId}, ${step.type}, ${i + 1}, ${status}, ${step.label ?? null},
|
|
6021
|
-
${stepConfigJson}::jsonb, ${logs}
|
|
6022
|
-
)
|
|
6023
|
-
RETURNING id` : sql`INSERT INTO release_step (
|
|
6024
|
-
release_id, step_type, step_order, status, step_label, logs
|
|
6025
|
-
) VALUES (
|
|
6026
|
-
${releaseId}, ${step.type}, ${i + 1}, ${status}, ${step.label ?? null}, ${logs}
|
|
6027
|
-
)
|
|
6028
|
-
RETURNING id`
|
|
6029
|
-
);
|
|
6030
|
-
const id = stepRows[0]?.id;
|
|
6031
|
-
if (id) stepIds.push(id);
|
|
6032
|
-
}
|
|
6033
|
-
return stepIds;
|
|
6034
|
-
}
|
|
6035
|
-
var ACTIVE_STATUSES = [
|
|
6036
|
-
"pending",
|
|
6037
|
-
"pending_approval",
|
|
6038
|
-
"reviewing",
|
|
6039
|
-
"running"
|
|
6040
|
-
];
|
|
6041
|
-
async function cancelReleaseRow(releaseId, reason, allowedStatuses) {
|
|
6042
|
-
const db2 = getDb();
|
|
6043
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6044
|
-
const releaseResult = await db2.execute(sql`
|
|
6045
|
-
UPDATE release
|
|
6046
|
-
SET status = 'cancelled',
|
|
6047
|
-
finished_at = ${now},
|
|
6048
|
-
error_message = ${reason},
|
|
6049
|
-
updated_at = ${now}
|
|
6050
|
-
WHERE id = ${releaseId}
|
|
6051
|
-
AND status IN ${allowedStatuses}
|
|
6052
|
-
RETURNING id
|
|
6053
|
-
`);
|
|
6054
|
-
if (releaseResult.length === 0) return false;
|
|
6055
|
-
await db2.execute(sql`
|
|
6056
|
-
UPDATE release_step
|
|
6057
|
-
SET status = 'skipped'
|
|
6058
|
-
WHERE release_id = ${releaseId}
|
|
6059
|
-
AND status = 'pending'
|
|
6060
|
-
`);
|
|
6061
|
-
return true;
|
|
6062
|
-
}
|
|
6063
|
-
async function cancelActiveReleasesForStage(stageId, reason) {
|
|
6064
|
-
const db2 = getDb();
|
|
6065
|
-
const active = await db2.execute(sql`
|
|
6066
|
-
SELECT id FROM release
|
|
6067
|
-
WHERE release_profile_stage_id = ${stageId}
|
|
6068
|
-
AND status IN ${ACTIVE_STATUSES}
|
|
6069
|
-
`);
|
|
6070
|
-
const cancelled = [];
|
|
6071
|
-
for (const row of active) {
|
|
6072
|
-
if (await cancelReleaseRow(row.id, reason, ACTIVE_STATUSES)) {
|
|
6073
|
-
cancelled.push(row.id);
|
|
6074
|
-
}
|
|
6075
|
-
}
|
|
6076
|
-
return cancelled;
|
|
6077
|
-
}
|
|
6078
|
-
function isPostgresUniqueViolation(err) {
|
|
6079
|
-
return typeof err === "object" && err !== null && "code" in err && err.code === "23505";
|
|
6080
|
-
}
|
|
6081
|
-
async function resolveUserDisplayName(userId) {
|
|
6082
|
-
const rows = await getDb().execute(sql`
|
|
6083
|
-
SELECT full_name, email
|
|
6084
|
-
FROM "user"
|
|
6085
|
-
WHERE id = ${userId}
|
|
6086
|
-
LIMIT 1
|
|
6087
|
-
`);
|
|
6088
|
-
const user = rows[0];
|
|
6089
|
-
if (!user) return null;
|
|
6090
|
-
const fullName = user.full_name?.trim();
|
|
6091
|
-
return fullName || user.email || null;
|
|
6092
|
-
}
|
|
6093
|
-
|
|
6094
|
-
// ../jobs/src/create-release-for-stage.ts
|
|
6095
|
-
var GITHUB_API2 = "https://api.github.com";
|
|
6096
|
-
var CreateReleaseError = class extends Error {
|
|
6097
|
-
constructor(message, code = "INTERNAL_SERVER_ERROR") {
|
|
6098
|
-
super(message);
|
|
6099
|
-
this.code = code;
|
|
6100
|
-
this.name = "CreateReleaseError";
|
|
6101
|
-
}
|
|
6102
|
-
};
|
|
6103
|
-
async function resolveBranchHeadSha(repoFullName, branch, token) {
|
|
6104
|
-
const headers = {
|
|
6105
|
-
Accept: "application/vnd.github.v3+json",
|
|
6106
|
-
Authorization: `Bearer ${token}`
|
|
6107
|
-
};
|
|
6108
|
-
const refRes = await fetch(
|
|
6109
|
-
`${GITHUB_API2}/repos/${repoFullName}/git/ref/heads/${branch}`,
|
|
6110
|
-
{ headers }
|
|
6111
|
-
);
|
|
6112
|
-
if (!refRes.ok) {
|
|
6113
|
-
throw new CreateReleaseError(
|
|
6114
|
-
`Could not find branch ${branch} to deploy from`,
|
|
6115
|
-
"BAD_REQUEST"
|
|
6116
|
-
);
|
|
6117
|
-
}
|
|
6118
|
-
const refData = await refRes.json();
|
|
6119
|
-
const sha = refData.object?.sha;
|
|
6120
|
-
if (!sha) {
|
|
6121
|
-
throw new CreateReleaseError(
|
|
6122
|
-
`Could not determine SHA for branch ${branch}`,
|
|
6123
|
-
"INTERNAL_SERVER_ERROR"
|
|
6124
|
-
);
|
|
6125
|
-
}
|
|
6126
|
-
return sha;
|
|
6127
|
-
}
|
|
6128
|
-
async function createReleaseForStage(userId, params) {
|
|
6129
|
-
const triggerType = params.triggerType ?? "manual";
|
|
6130
|
-
const db2 = getDb();
|
|
6131
|
-
const stageRows = await db2.execute(
|
|
6132
|
-
sql`SELECT * FROM release_profile_stage WHERE id = ${params.stageId} LIMIT 1`
|
|
6133
|
-
);
|
|
6134
|
-
const stage = stageRows[0];
|
|
6135
|
-
if (!stage) {
|
|
6136
|
-
throw new CreateReleaseError("Stage not found", "NOT_FOUND");
|
|
6137
|
-
}
|
|
6138
|
-
const profileRows = await db2.execute(
|
|
6139
|
-
sql`SELECT * FROM release_profile WHERE id = ${stage.release_profile_id} LIMIT 1`
|
|
6140
|
-
);
|
|
6141
|
-
const profile = profileRows[0];
|
|
6142
|
-
if (!profile) {
|
|
6143
|
-
throw new CreateReleaseError("Release profile not found", "NOT_FOUND");
|
|
6144
|
-
}
|
|
6145
|
-
if (!profile.enabled || !stage.enabled) {
|
|
6146
|
-
throw new CreateReleaseError(
|
|
6147
|
-
"Profile or stage is disabled",
|
|
6148
|
-
"BAD_REQUEST"
|
|
6149
|
-
);
|
|
6150
|
-
}
|
|
6151
|
-
const deployBranch = stage.trigger_branch?.trim() || stage.release_branch?.trim() || null;
|
|
6152
|
-
if (!deployBranch) {
|
|
6153
|
-
throw new CreateReleaseError(
|
|
6154
|
-
"Stage has no release or trigger branch configured",
|
|
6155
|
-
"BAD_REQUEST"
|
|
6156
|
-
);
|
|
6157
|
-
}
|
|
6158
|
-
const { token } = await resolveReleaseGitHubToken(
|
|
6159
|
-
profile.github_token_id ?? null
|
|
6160
|
-
);
|
|
6161
|
-
if (!token) {
|
|
6162
|
-
throw new CreateReleaseError(
|
|
6163
|
-
"GitHub token is not configured",
|
|
6164
|
-
"PRECONDITION_FAILED"
|
|
6165
|
-
);
|
|
6166
|
-
}
|
|
6167
|
-
const triggerSha = await resolveBranchHeadSha(
|
|
6168
|
-
profile.repo_full_name,
|
|
6169
|
-
deployBranch,
|
|
6170
|
-
token
|
|
6171
|
-
);
|
|
6172
|
-
if (params.skipIfActiveSameSha) {
|
|
6173
|
-
const existing = await db2.execute(sql`
|
|
6174
|
-
SELECT
|
|
6175
|
-
r.id,
|
|
6176
|
-
r.status,
|
|
6177
|
-
r.version,
|
|
6178
|
-
r.trigger_author_name,
|
|
6179
|
-
COUNT(rs.id)::int AS steps_count
|
|
6180
|
-
FROM release r
|
|
6181
|
-
LEFT JOIN release_step rs ON rs.release_id = r.id
|
|
6182
|
-
WHERE r.release_profile_stage_id = ${params.stageId}
|
|
6183
|
-
AND r.trigger_sha = ${triggerSha}
|
|
6184
|
-
AND r.status IN ('pending', 'pending_approval', 'running', 'reviewing', 'success')
|
|
6185
|
-
GROUP BY r.id
|
|
6186
|
-
ORDER BY r.created_at DESC
|
|
6187
|
-
LIMIT 1
|
|
6188
|
-
`);
|
|
6189
|
-
if (existing[0]) {
|
|
6190
|
-
return {
|
|
6191
|
-
id: existing[0].id,
|
|
6192
|
-
status: existing[0].status ?? "running",
|
|
6193
|
-
version: existing[0].version ?? null,
|
|
6194
|
-
stepsCount: Number(existing[0].steps_count ?? 0),
|
|
6195
|
-
authorName: existing[0].trigger_author_name ?? null
|
|
6196
|
-
};
|
|
6197
|
-
}
|
|
6198
|
-
}
|
|
6199
|
-
await cancelActiveReleasesForStage(
|
|
6200
|
-
params.stageId,
|
|
6201
|
-
"Superseded by newer manual trigger"
|
|
6202
|
-
);
|
|
6203
|
-
let triggerAuthorName = params.triggerAuthorName?.trim() || null;
|
|
6204
|
-
if (!triggerAuthorName && userId) {
|
|
6205
|
-
triggerAuthorName = await resolveUserDisplayName(userId);
|
|
6206
|
-
}
|
|
6207
|
-
const enabledSteps = buildStepList(
|
|
6208
|
-
stage,
|
|
6209
|
-
profile,
|
|
6210
|
-
await resolveBuildStepListOptions({
|
|
6211
|
-
profile,
|
|
6212
|
-
triggerSha,
|
|
6213
|
-
githubToken: token,
|
|
6214
|
-
isManual: triggerType === "manual" || triggerType === "mcp",
|
|
6215
|
-
stageId: params.stageId,
|
|
6216
|
-
deployRetryScripts: stage.deploy_retry_scripts
|
|
6217
|
-
})
|
|
6218
|
-
);
|
|
6219
|
-
if (enabledSteps.length === 0) {
|
|
6220
|
-
throw new CreateReleaseError(
|
|
6221
|
-
"No pipeline steps configured for this stage (enable a PM2/Docker stage_app or deployment_project_server)",
|
|
6222
|
-
"PRECONDITION_FAILED"
|
|
6223
|
-
);
|
|
6224
|
-
}
|
|
6225
|
-
const initialStatus = "running";
|
|
6226
|
-
let release;
|
|
6227
|
-
try {
|
|
6228
|
-
const inserted = await db2.execute(sql`
|
|
6229
|
-
INSERT INTO release (
|
|
6230
|
-
release_profile_id, release_profile_stage_id, status, started_at,
|
|
6231
|
-
version, version_type, trigger_type, skip_pr_flow, created_by,
|
|
6232
|
-
trigger_kind, trigger_branch, trigger_sha, source_commit,
|
|
6233
|
-
trigger_author_name
|
|
6234
|
-
) VALUES (
|
|
6235
|
-
${profile.id}, ${params.stageId}, ${initialStatus}, ${(/* @__PURE__ */ new Date()).toISOString()},
|
|
6236
|
-
NULL, ${params.versionType}, ${triggerType}, ${params.skipPrFlow}, ${userId},
|
|
6237
|
-
'branch', ${deployBranch}, ${triggerSha}, ${triggerSha},
|
|
6238
|
-
${triggerAuthorName}
|
|
6239
|
-
)
|
|
6240
|
-
RETURNING *
|
|
6241
|
-
`);
|
|
6242
|
-
if (!inserted[0]) {
|
|
6243
|
-
throw new Error("INSERT did not return a row");
|
|
6244
|
-
}
|
|
6245
|
-
release = inserted[0];
|
|
6246
|
-
} catch (err) {
|
|
6247
|
-
if (isPostgresUniqueViolation(err)) {
|
|
6248
|
-
throw new CreateReleaseError(
|
|
6249
|
-
"A release is already running for this stage.",
|
|
6250
|
-
"CONFLICT"
|
|
6251
|
-
);
|
|
6252
|
-
}
|
|
6253
|
-
throw new CreateReleaseError(
|
|
6254
|
-
`Failed to create release: ${err instanceof Error ? err.message : String(err)}`,
|
|
6255
|
-
"INTERNAL_SERVER_ERROR"
|
|
6256
|
-
);
|
|
6257
|
-
}
|
|
6258
|
-
const stepIds = [];
|
|
6259
|
-
try {
|
|
6260
|
-
stepIds.push(...await insertReleaseSteps(release.id, enabledSteps));
|
|
6261
|
-
} catch (err) {
|
|
6262
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
6263
|
-
await db2.execute(sql`
|
|
6264
|
-
UPDATE release
|
|
6265
|
-
SET status = 'failed',
|
|
6266
|
-
error_message = ${`Failed to create steps: ${msg}`},
|
|
6267
|
-
finished_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
6268
|
-
WHERE id = ${release.id}
|
|
6269
|
-
`);
|
|
6270
|
-
throw new CreateReleaseError(
|
|
6271
|
-
`Failed to create release steps: ${msg}`,
|
|
6272
|
-
"INTERNAL_SERVER_ERROR"
|
|
6273
|
-
);
|
|
6274
|
-
}
|
|
6275
|
-
try {
|
|
6276
|
-
await triggerPipeline({
|
|
6277
|
-
releaseId: release.id,
|
|
6278
|
-
profileId: profile.id,
|
|
6279
|
-
stageId: stage.id,
|
|
6280
|
-
stepIds,
|
|
6281
|
-
version: null
|
|
6282
|
-
});
|
|
6283
|
-
} catch (err) {
|
|
6284
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
6285
|
-
await db2.execute(sql`
|
|
6286
|
-
UPDATE release
|
|
6287
|
-
SET status = 'failed',
|
|
6288
|
-
error_message = ${`Failed to start pipeline: ${msg}`},
|
|
6289
|
-
finished_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
6290
|
-
WHERE id = ${release.id}
|
|
6291
|
-
`);
|
|
6292
|
-
throw new CreateReleaseError(
|
|
6293
|
-
`Failed to start pipeline: ${msg}`,
|
|
6294
|
-
"INTERNAL_SERVER_ERROR"
|
|
6295
|
-
);
|
|
6296
|
-
}
|
|
6297
|
-
return {
|
|
6298
|
-
id: release.id,
|
|
6299
|
-
status: initialStatus,
|
|
6300
|
-
version: null,
|
|
6301
|
-
stepsCount: enabledSteps.length,
|
|
6302
|
-
authorName: triggerAuthorName
|
|
6303
|
-
};
|
|
6304
|
-
}
|
|
6305
|
-
var GITHUB_API3 = "https://api.github.com";
|
|
6306
5749
|
var ReleaseProfileQuickCreateError = class extends Error {
|
|
6307
5750
|
constructor(message, code = "INTERNAL_SERVER_ERROR") {
|
|
6308
5751
|
super(message);
|
|
@@ -6330,7 +5773,7 @@ async function scanRepoForApps(repoFullName, githubToken2, ref = "main") {
|
|
|
6330
5773
|
};
|
|
6331
5774
|
async function fetchContents(path) {
|
|
6332
5775
|
const res = await fetch(
|
|
6333
|
-
`${
|
|
5776
|
+
`${GITHUB_API}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
|
|
6334
5777
|
{ headers }
|
|
6335
5778
|
);
|
|
6336
5779
|
if (!res.ok) return null;
|
|
@@ -6339,7 +5782,7 @@ async function scanRepoForApps(repoFullName, githubToken2, ref = "main") {
|
|
|
6339
5782
|
}
|
|
6340
5783
|
async function fileExists(path) {
|
|
6341
5784
|
const res = await fetch(
|
|
6342
|
-
`${
|
|
5785
|
+
`${GITHUB_API}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
|
|
6343
5786
|
{ headers, method: "HEAD" }
|
|
6344
5787
|
);
|
|
6345
5788
|
return res.ok;
|
|
@@ -6478,20 +5921,20 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
|
|
|
6478
5921
|
"Content-Type": "application/json"
|
|
6479
5922
|
};
|
|
6480
5923
|
const checkRes = await fetch(
|
|
6481
|
-
`${
|
|
5924
|
+
`${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${branchName}`,
|
|
6482
5925
|
{ headers }
|
|
6483
5926
|
);
|
|
6484
5927
|
if (checkRes.ok) return;
|
|
6485
5928
|
let sha;
|
|
6486
5929
|
const sourceRes = await fetch(
|
|
6487
|
-
`${
|
|
5930
|
+
`${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${fromBranch}`,
|
|
6488
5931
|
{ headers }
|
|
6489
5932
|
);
|
|
6490
5933
|
if (sourceRes.ok) {
|
|
6491
5934
|
const data = await sourceRes.json();
|
|
6492
5935
|
sha = data.object?.sha;
|
|
6493
5936
|
} else {
|
|
6494
|
-
const repoRes = await fetch(`${
|
|
5937
|
+
const repoRes = await fetch(`${GITHUB_API}/repos/${repoFullName}`, {
|
|
6495
5938
|
headers
|
|
6496
5939
|
});
|
|
6497
5940
|
if (!repoRes.ok) {
|
|
@@ -6500,7 +5943,7 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
|
|
|
6500
5943
|
const repo = await repoRes.json();
|
|
6501
5944
|
const defaultBranch = repo.default_branch ?? "main";
|
|
6502
5945
|
const defaultRes = await fetch(
|
|
6503
|
-
`${
|
|
5946
|
+
`${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${defaultBranch}`,
|
|
6504
5947
|
{ headers }
|
|
6505
5948
|
);
|
|
6506
5949
|
if (!defaultRes.ok) {
|
|
@@ -6513,7 +5956,7 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
|
|
|
6513
5956
|
throw new Error(`Could not determine SHA to create branch ${branchName}`);
|
|
6514
5957
|
}
|
|
6515
5958
|
const createRes = await fetch(
|
|
6516
|
-
`${
|
|
5959
|
+
`${GITHUB_API}/repos/${repoFullName}/git/refs`,
|
|
6517
5960
|
{
|
|
6518
5961
|
method: "POST",
|
|
6519
5962
|
headers,
|
|
@@ -6759,6 +6202,7 @@ var apiKey = getArg2("api-key") || process.env.MG_DASHBOARD_API_KEY;
|
|
|
6759
6202
|
var sshKeyPath = getArg2("ssh-key") || process.env.MG_DASHBOARD_SSH_KEY;
|
|
6760
6203
|
var databaseUrl = getArg2("database-url") || process.env.DATABASE_PRIMARY_POOLER_URL || process.env.DATABASE_PRIMARY_URL;
|
|
6761
6204
|
var encryptionKey = getArg2("encryption-key") || process.env.ENCRYPTION_KEY;
|
|
6205
|
+
var dashboardBaseUrl = (getArg2("dashboard-url") || process.env.MG_DASHBOARD_BASE_URL || "https://dashboard.mgsoftware.nl").replace(/\/$/, "");
|
|
6762
6206
|
var mijnhostApiKey = getArg2("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
|
|
6763
6207
|
function isMijnhostViaSshProxyEnabled() {
|
|
6764
6208
|
const arg = getArg2("mijnhost-via-ssh-proxy");
|
|
@@ -6975,7 +6419,6 @@ var TOOL_MODULE_MAP = {
|
|
|
6975
6419
|
"docker-logs": "ssh_servers",
|
|
6976
6420
|
"docker-exec": "ssh_servers",
|
|
6977
6421
|
"docker-compose": "ssh_servers",
|
|
6978
|
-
"wait-for": "ssh_servers",
|
|
6979
6422
|
"db-discover": "ssh_servers",
|
|
6980
6423
|
"db-tables": "ssh_servers",
|
|
6981
6424
|
"db-query": "ssh_servers",
|
|
@@ -6985,10 +6428,8 @@ var TOOL_MODULE_MAP = {
|
|
|
6985
6428
|
"env-list": "ci_cd",
|
|
6986
6429
|
"env-get": "ci_cd",
|
|
6987
6430
|
"env-store": "ci_cd",
|
|
6988
|
-
"release-trigger": "ci_cd",
|
|
6989
6431
|
"release-profile-list": "ci_cd",
|
|
6990
|
-
"release-profile
|
|
6991
|
-
"release-profile-update": "ci_cd",
|
|
6432
|
+
"release-profile": "ci_cd",
|
|
6992
6433
|
"domain-list": "domains",
|
|
6993
6434
|
"dns-list": "domains",
|
|
6994
6435
|
"dns-record": "domains",
|
|
@@ -9550,11 +8991,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
|
|
|
9550
8991
|
applied_by TEXT
|
|
9551
8992
|
);
|
|
9552
8993
|
`.trim();
|
|
9553
|
-
function normaliseMigrationSql(
|
|
9554
|
-
return
|
|
8994
|
+
function normaliseMigrationSql(sql23) {
|
|
8995
|
+
return sql23.replace(/\r\n/g, "\n").trim() + "\n";
|
|
9555
8996
|
}
|
|
9556
|
-
function migrationSha256(
|
|
9557
|
-
return createHash("sha256").update(normaliseMigrationSql(
|
|
8997
|
+
function migrationSha256(sql23) {
|
|
8998
|
+
return createHash("sha256").update(normaliseMigrationSql(sql23), "utf8").digest("hex");
|
|
9558
8999
|
}
|
|
9559
9000
|
function dollarQuoteTag(value) {
|
|
9560
9001
|
let tag = "_mcp";
|
|
@@ -10294,48 +9735,6 @@ var TOOLS = [
|
|
|
10294
9735
|
required: ["serverId", "projectPath", "action"]
|
|
10295
9736
|
}
|
|
10296
9737
|
},
|
|
10297
|
-
{
|
|
10298
|
-
name: "wait-for",
|
|
10299
|
-
description: 'Poll until a condition holds (or timeout). Replaces 30+ ad-hoc loops the LLM would otherwise build itself. Four condition types:\n- `url`: `target: "https://..."`, `until: 200` (HTTP status, default 200). 10s per probe.\n- `container-health`: `serverId`, `target: "container-name"`, `until: "healthy" | "running" | "exited"` (default "healthy"). Uses `docker inspect`.\n- `file-exists`: `serverId`, `target: "/path/to/file"` \u2014 SSH stat check.\n- `sftp-exists`: same as file-exists but with `bucket: "name"` instead of serverId for R2.\n\nReturns a per-attempt trail so you can see how long it took to converge.',
|
|
10300
|
-
inputSchema: {
|
|
10301
|
-
type: "object",
|
|
10302
|
-
properties: {
|
|
10303
|
-
type: {
|
|
10304
|
-
type: "string",
|
|
10305
|
-
enum: ["url", "container-health", "file-exists", "sftp-exists"],
|
|
10306
|
-
description: "Condition type to wait on."
|
|
10307
|
-
},
|
|
10308
|
-
target: {
|
|
10309
|
-
type: "string",
|
|
10310
|
-
description: "Target identifier: URL, container name, file path, or R2 key (depends on type)."
|
|
10311
|
-
},
|
|
10312
|
-
until: {
|
|
10313
|
-
description: 'Expected end-state (depends on type): for url an HTTP status (default 200), for container-health a state string (default "healthy"). Omit for file-exists/sftp-exists.'
|
|
10314
|
-
},
|
|
10315
|
-
serverId: {
|
|
10316
|
-
type: "string",
|
|
10317
|
-
description: "SSH server UUID (required for container-health and file-exists)."
|
|
10318
|
-
},
|
|
10319
|
-
bucket: {
|
|
10320
|
-
type: "string",
|
|
10321
|
-
description: "R2 bucket name (used with sftp-exists when checking an R2 object)."
|
|
10322
|
-
},
|
|
10323
|
-
path: {
|
|
10324
|
-
type: "string",
|
|
10325
|
-
description: "Alias for `target` for file-exists/sftp-exists."
|
|
10326
|
-
},
|
|
10327
|
-
intervalSeconds: {
|
|
10328
|
-
type: "number",
|
|
10329
|
-
description: "Poll interval in seconds (default 3, max 60)."
|
|
10330
|
-
},
|
|
10331
|
-
timeout: {
|
|
10332
|
-
type: "number",
|
|
10333
|
-
description: "Total deadline in seconds (default 120, max 1800)."
|
|
10334
|
-
}
|
|
10335
|
-
},
|
|
10336
|
-
required: ["type", "target"]
|
|
10337
|
-
}
|
|
10338
|
-
},
|
|
10339
9738
|
{
|
|
10340
9739
|
name: "db-discover",
|
|
10341
9740
|
description: 'Discover databases available on an SSH server. Use this first to find what is reachable before running other db-* tools.\n\nModes (pass via `include` array, default `["www"]`):\n- `"www"` \u2014 scan `/var/www` for WordPress / PrestaShop / Laravel / generic .env apps. Returns sitePath, db name, user, host, port.\n- `"postgres-containers"` \u2014 enumerate running Docker containers that expose `POSTGRES_USER` / `POSTGRES_DB` env vars (vanilla `postgres:*` / `pgvector/*` images). Returns container name, db, user, host port mapping, and whether a password is set. Use the returned (container, db, user) tuple directly with `db-query containerName=\u2026` or `db-apply-migration`.\n\nPass multiple modes to scan in parallel: `{ include: ["www", "postgres-containers"] }`.',
|
|
@@ -10594,33 +9993,6 @@ var TOOLS = [
|
|
|
10594
9993
|
required: ["appName", "environment", "content"]
|
|
10595
9994
|
}
|
|
10596
9995
|
},
|
|
10597
|
-
{
|
|
10598
|
-
name: "release-trigger",
|
|
10599
|
-
description: "Start a release/deploy pipeline for a release profile stage. Attributes the deploy to the MCP API key owner. Requires ci_cd permission.",
|
|
10600
|
-
inputSchema: {
|
|
10601
|
-
type: "object",
|
|
10602
|
-
properties: {
|
|
10603
|
-
releaseProfile: {
|
|
10604
|
-
type: "string",
|
|
10605
|
-
description: "Release profile name (usually matches the project folder name or git repo name, e.g. mg-dashboard)."
|
|
10606
|
-
},
|
|
10607
|
-
stage: {
|
|
10608
|
-
type: "string",
|
|
10609
|
-
description: "Stage slug or name (e.g. dev, production). Required when the profile has multiple enabled stages."
|
|
10610
|
-
},
|
|
10611
|
-
versionType: {
|
|
10612
|
-
type: "string",
|
|
10613
|
-
enum: ["major", "minor", "patch"],
|
|
10614
|
-
description: "Semver bump type when versioning applies (default: minor)."
|
|
10615
|
-
},
|
|
10616
|
-
skipPrFlow: {
|
|
10617
|
-
type: "boolean",
|
|
10618
|
-
description: "Skip PR approval flow (default: false)."
|
|
10619
|
-
}
|
|
10620
|
-
},
|
|
10621
|
-
required: ["releaseProfile"]
|
|
10622
|
-
}
|
|
10623
|
-
},
|
|
10624
9996
|
{
|
|
10625
9997
|
name: "release-profile-list",
|
|
10626
9998
|
description: "List all release profiles with repo, enabled state, release type, GitHub key and a summary of their stages. Requires ci_cd permission.",
|
|
@@ -10631,67 +10003,55 @@ var TOOLS = [
|
|
|
10631
10003
|
}
|
|
10632
10004
|
},
|
|
10633
10005
|
{
|
|
10634
|
-
name: "release-profile
|
|
10635
|
-
description:
|
|
10006
|
+
name: "release-profile",
|
|
10007
|
+
description: 'Create or update a release profile. Pick the operation with `action`:\n- "create": one-step creation \u2014 scans the GitHub repo, detects apps (Next.js/Docker/PM2), creates the profile plus dev/prod stages with smart defaults (dev: release-dev + PR webhook + auto-approve, prod: release-prod + manual + DB backup) and seeds the stage apps. Release branches are created on GitHub automatically. Required: repoFullName. Optional: githubKey, name, releaseType. Assign deployment servers afterwards in the dashboard (Releases \u2192 Profiles).\n- "update": change settings of an existing profile located by `releaseProfile` (name or repo). Changing releaseType reconfigures the stages (missing stages are created with smart defaults, out-of-scope stages are disabled). Required: releaseProfile. Optional: name, enabled, releaseType, githubKey, useChangesBranch, hasTriggerDev, workDirectory.\n\nRequires ci_cd permission.',
|
|
10636
10008
|
inputSchema: {
|
|
10637
10009
|
type: "object",
|
|
10638
10010
|
properties: {
|
|
10011
|
+
action: {
|
|
10012
|
+
type: "string",
|
|
10013
|
+
enum: ["create", "update"],
|
|
10014
|
+
description: "Which operation to perform."
|
|
10015
|
+
},
|
|
10639
10016
|
repoFullName: {
|
|
10640
10017
|
type: "string",
|
|
10641
|
-
description: 'Repository in "owner/name" format (e.g. MGSoftwareBV/mg-dashboard).'
|
|
10018
|
+
description: 'Repository in "owner/name" format (e.g. MGSoftwareBV/mg-dashboard). Required for action="create".'
|
|
10642
10019
|
},
|
|
10643
|
-
|
|
10020
|
+
releaseProfile: {
|
|
10644
10021
|
type: "string",
|
|
10645
|
-
description: '
|
|
10022
|
+
description: 'Profile name or repo full name to locate the profile. Required for action="update".'
|
|
10646
10023
|
},
|
|
10647
10024
|
name: {
|
|
10648
10025
|
type: "string",
|
|
10649
|
-
description: 'Profile name override
|
|
10026
|
+
description: 'Profile name. For create: name override (default: title-cased repo name, e.g. mg-dashboard \u2192 "Mg Dashboard"). For update: new profile name.'
|
|
10650
10027
|
},
|
|
10651
|
-
|
|
10652
|
-
type: "
|
|
10653
|
-
|
|
10654
|
-
description: "Which stages to create (default: both = dev + prod)."
|
|
10655
|
-
}
|
|
10656
|
-
},
|
|
10657
|
-
required: ["repoFullName"]
|
|
10658
|
-
}
|
|
10659
|
-
},
|
|
10660
|
-
{
|
|
10661
|
-
name: "release-profile-update",
|
|
10662
|
-
description: "Update release profile settings. Locate the profile by name or repo. Changing releaseType reconfigures the stages (missing stages are created with smart defaults, out-of-scope stages are disabled). Requires ci_cd permission.",
|
|
10663
|
-
inputSchema: {
|
|
10664
|
-
type: "object",
|
|
10665
|
-
properties: {
|
|
10666
|
-
releaseProfile: {
|
|
10667
|
-
type: "string",
|
|
10668
|
-
description: "Profile name or repo full name to locate the profile."
|
|
10028
|
+
enabled: {
|
|
10029
|
+
type: "boolean",
|
|
10030
|
+
description: "Enable/disable the profile (update only)."
|
|
10669
10031
|
},
|
|
10670
|
-
name: { type: "string", description: "New profile name." },
|
|
10671
|
-
enabled: { type: "boolean", description: "Enable/disable the profile." },
|
|
10672
10032
|
releaseType: {
|
|
10673
10033
|
type: "string",
|
|
10674
10034
|
enum: ["dev_only", "prod_only", "both"],
|
|
10675
|
-
description: "
|
|
10035
|
+
description: "Which stages exist (default for create: both = dev + prod). Changing it on update reconfigures the stages."
|
|
10676
10036
|
},
|
|
10677
10037
|
githubKey: {
|
|
10678
10038
|
type: "string",
|
|
10679
|
-
description: 'Named GitHub key (github_token label or owner).
|
|
10039
|
+
description: 'Named GitHub key (github_token label or owner, e.g. "AVARC Solutions") used for all GitHub API calls for this profile. Omit for the default MG Software token. On update, pass "default" to reset to the default token.'
|
|
10680
10040
|
},
|
|
10681
10041
|
useChangesBranch: {
|
|
10682
10042
|
type: "boolean",
|
|
10683
|
-
description: "Use a long-lived -changes accumulation branch."
|
|
10043
|
+
description: "Use a long-lived -changes accumulation branch (update only)."
|
|
10684
10044
|
},
|
|
10685
10045
|
hasTriggerDev: {
|
|
10686
10046
|
type: "boolean",
|
|
10687
|
-
description: "Deploy Trigger.dev jobs during production releases."
|
|
10047
|
+
description: "Deploy Trigger.dev jobs during production releases (update only)."
|
|
10688
10048
|
},
|
|
10689
10049
|
workDirectory: {
|
|
10690
10050
|
type: "string",
|
|
10691
|
-
description: 'Working directory within the repo (e.g. ".").'
|
|
10051
|
+
description: 'Working directory within the repo (e.g. "."). Update only.'
|
|
10692
10052
|
}
|
|
10693
10053
|
},
|
|
10694
|
-
required: ["
|
|
10054
|
+
required: ["action"]
|
|
10695
10055
|
}
|
|
10696
10056
|
},
|
|
10697
10057
|
{
|
|
@@ -10790,6 +10150,34 @@ var TOOLS = [
|
|
|
10790
10150
|
required: ["action", "domain", "type", "name"]
|
|
10791
10151
|
}
|
|
10792
10152
|
},
|
|
10153
|
+
// ----- Team memory -----
|
|
10154
|
+
{
|
|
10155
|
+
name: "search-team-memory",
|
|
10156
|
+
description: "Search the team's ENTIRE past Cursor history (every developer, every project) for how something was handled before. Use this FIRST, before investigating from scratch, whenever you: hit a non-trivial bug or error, are about to build something that may have been done before, need a project-specific convention/gotcha, or the user asks 'have we done X', 'how did we fix Y', or 'did we solve this already'. Hybrid semantic + keyword search over mirrored conversations. Returns ranked past chats with repo, date, a solution snippet and a similarity score. Phrase the query as the problem in natural language (e.g. 'release pipeline PM2 deploy fails with module not found').",
|
|
10157
|
+
inputSchema: {
|
|
10158
|
+
type: "object",
|
|
10159
|
+
properties: {
|
|
10160
|
+
query: {
|
|
10161
|
+
type: "string",
|
|
10162
|
+
description: "The problem/topic in natural language. Include error text, tool/module names, or symptoms for best recall."
|
|
10163
|
+
},
|
|
10164
|
+
repo: {
|
|
10165
|
+
type: "string",
|
|
10166
|
+
description: "Optional repo filter, e.g. 'github.com/MGSoftwareBV/mg-dashboard'. Omit to search across all projects."
|
|
10167
|
+
},
|
|
10168
|
+
scope: {
|
|
10169
|
+
type: "string",
|
|
10170
|
+
enum: ["team", "mine"],
|
|
10171
|
+
description: "'team' (default) searches everyone's history; 'mine' restricts to the calling user's own conversations."
|
|
10172
|
+
},
|
|
10173
|
+
limit: {
|
|
10174
|
+
type: "number",
|
|
10175
|
+
description: "Max results to return (1-25, default 8)."
|
|
10176
|
+
}
|
|
10177
|
+
},
|
|
10178
|
+
required: ["query"]
|
|
10179
|
+
}
|
|
10180
|
+
},
|
|
10793
10181
|
// ----- Trigger.dev -----
|
|
10794
10182
|
...TRIGGER_TOOLS
|
|
10795
10183
|
];
|
|
@@ -10861,6 +10249,65 @@ async function executeToolCall(name, a, _serverId) {
|
|
|
10861
10249
|
const ctx = authContext;
|
|
10862
10250
|
try {
|
|
10863
10251
|
switch (name) {
|
|
10252
|
+
// ----- Team memory -----
|
|
10253
|
+
case "search-team-memory": {
|
|
10254
|
+
const query = typeof a.query === "string" ? a.query.trim() : "";
|
|
10255
|
+
if (!query) {
|
|
10256
|
+
return { content: [{ type: "text", text: "Error: query is required" }] };
|
|
10257
|
+
}
|
|
10258
|
+
const res = await fetch(`${dashboardBaseUrl}/api/team-memory/search`, {
|
|
10259
|
+
method: "POST",
|
|
10260
|
+
headers: {
|
|
10261
|
+
"content-type": "application/json",
|
|
10262
|
+
authorization: `Bearer ${apiKey}`
|
|
10263
|
+
},
|
|
10264
|
+
body: JSON.stringify({
|
|
10265
|
+
query,
|
|
10266
|
+
repo: typeof a.repo === "string" ? a.repo : void 0,
|
|
10267
|
+
scope: a.scope === "mine" ? "mine" : "team",
|
|
10268
|
+
limit: typeof a.limit === "number" ? a.limit : void 0
|
|
10269
|
+
})
|
|
10270
|
+
});
|
|
10271
|
+
if (!res.ok) {
|
|
10272
|
+
const detail = await res.text().catch(() => "");
|
|
10273
|
+
return {
|
|
10274
|
+
content: [
|
|
10275
|
+
{
|
|
10276
|
+
type: "text",
|
|
10277
|
+
text: `Error: team-memory search failed (${res.status}). ${detail.slice(0, 300)}`
|
|
10278
|
+
}
|
|
10279
|
+
]
|
|
10280
|
+
};
|
|
10281
|
+
}
|
|
10282
|
+
const data = await res.json();
|
|
10283
|
+
if (data.count === 0) {
|
|
10284
|
+
return {
|
|
10285
|
+
content: [
|
|
10286
|
+
{
|
|
10287
|
+
type: "text",
|
|
10288
|
+
text: `No prior team conversations matched "${query}". Proceed from scratch.`
|
|
10289
|
+
}
|
|
10290
|
+
]
|
|
10291
|
+
};
|
|
10292
|
+
}
|
|
10293
|
+
const lines = data.hits.map((hit, index5) => {
|
|
10294
|
+
const when = hit.lastMessageAt ? new Date(hit.lastMessageAt).toISOString().slice(0, 10) : "unknown date";
|
|
10295
|
+
const repo = hit.repo ?? "unknown repo";
|
|
10296
|
+
const sim = hit.similarity !== null ? ` \xB7 ${(hit.similarity * 100).toFixed(0)}% match` : "";
|
|
10297
|
+
return `${index5 + 1}. [${repo} \xB7 ${when}${sim}] ${hit.title}
|
|
10298
|
+
${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 500)}`;
|
|
10299
|
+
});
|
|
10300
|
+
return {
|
|
10301
|
+
content: [
|
|
10302
|
+
{
|
|
10303
|
+
type: "text",
|
|
10304
|
+
text: `Found ${data.count} prior conversation(s) (${data.mode} search) \u2014 reuse this before solving from scratch:
|
|
10305
|
+
|
|
10306
|
+
` + lines.join("\n\n")
|
|
10307
|
+
}
|
|
10308
|
+
]
|
|
10309
|
+
};
|
|
10310
|
+
}
|
|
10864
10311
|
// ----- Servers -----
|
|
10865
10312
|
case "list-servers": {
|
|
10866
10313
|
const data = ctx.allowedServerIds !== null ? await db.execute(sql`
|
|
@@ -12156,163 +11603,6 @@ ${stderrOut}`);
|
|
|
12156
11603
|
${result.stderr}`);
|
|
12157
11604
|
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
12158
11605
|
}
|
|
12159
|
-
// ----- wait-for: poll until a condition holds (one tool, four types) -----
|
|
12160
|
-
case "wait-for": {
|
|
12161
|
-
const type = String(a.type || "");
|
|
12162
|
-
if (!["url", "container-health", "file-exists", "sftp-exists"].includes(
|
|
12163
|
-
type
|
|
12164
|
-
)) {
|
|
12165
|
-
return {
|
|
12166
|
-
content: [
|
|
12167
|
-
{
|
|
12168
|
-
type: "text",
|
|
12169
|
-
text: "Error: type must be one of: url, container-health, file-exists, sftp-exists"
|
|
12170
|
-
}
|
|
12171
|
-
]
|
|
12172
|
-
};
|
|
12173
|
-
}
|
|
12174
|
-
const timeoutSeconds = Math.max(
|
|
12175
|
-
1,
|
|
12176
|
-
Math.min(1800, Number(a.timeout) || 120)
|
|
12177
|
-
);
|
|
12178
|
-
const intervalSeconds = Math.max(
|
|
12179
|
-
1,
|
|
12180
|
-
Math.min(60, Number(a.intervalSeconds) || 3)
|
|
12181
|
-
);
|
|
12182
|
-
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
12183
|
-
const trail = [];
|
|
12184
|
-
const check = async () => {
|
|
12185
|
-
if (type === "url") {
|
|
12186
|
-
const target = String(a.target || "");
|
|
12187
|
-
if (!target) return { ok: false, observed: "no target url" };
|
|
12188
|
-
const expectStatus = a.until !== void 0 ? Number(a.until) : 200;
|
|
12189
|
-
try {
|
|
12190
|
-
const ctrl = new AbortController();
|
|
12191
|
-
const t = setTimeout(() => ctrl.abort(), 1e4);
|
|
12192
|
-
const res = await fetch(target, {
|
|
12193
|
-
signal: ctrl.signal,
|
|
12194
|
-
redirect: "manual"
|
|
12195
|
-
});
|
|
12196
|
-
clearTimeout(t);
|
|
12197
|
-
const ok2 = res.status === expectStatus || expectStatus === 200 && res.status >= 200 && res.status < 300;
|
|
12198
|
-
return { ok: ok2, observed: `HTTP ${res.status}` };
|
|
12199
|
-
} catch (e) {
|
|
12200
|
-
return {
|
|
12201
|
-
ok: false,
|
|
12202
|
-
observed: e instanceof Error ? e.message : String(e)
|
|
12203
|
-
};
|
|
12204
|
-
}
|
|
12205
|
-
}
|
|
12206
|
-
if (type === "container-health") {
|
|
12207
|
-
const containerName = String(a.target || "").replace(
|
|
12208
|
-
/[^a-zA-Z0-9._-]/g,
|
|
12209
|
-
""
|
|
12210
|
-
);
|
|
12211
|
-
const until = String(a.until || "healthy");
|
|
12212
|
-
if (!containerName)
|
|
12213
|
-
return { ok: false, observed: "no container name" };
|
|
12214
|
-
if (!a.serverId)
|
|
12215
|
-
return {
|
|
12216
|
-
ok: false,
|
|
12217
|
-
observed: "serverId is required for container-health"
|
|
12218
|
-
};
|
|
12219
|
-
const { conn: conn2, proxy: proxy2 } = await getServerConnection(
|
|
12220
|
-
String(a.serverId)
|
|
12221
|
-
);
|
|
12222
|
-
const cmd2 = `docker inspect --format '{{json .State}}' ${containerName} 2>&1`;
|
|
12223
|
-
const r2 = await sshExec(conn2, cmd2, proxy2);
|
|
12224
|
-
if (r2.exitCode !== 0)
|
|
12225
|
-
return {
|
|
12226
|
-
ok: false,
|
|
12227
|
-
observed: r2.stderr.trim().slice(0, 80) || "inspect failed"
|
|
12228
|
-
};
|
|
12229
|
-
try {
|
|
12230
|
-
const state = JSON.parse(r2.stdout.trim());
|
|
12231
|
-
const healthStatus = state.Health?.Status;
|
|
12232
|
-
if (until === "healthy") {
|
|
12233
|
-
return {
|
|
12234
|
-
ok: healthStatus === "healthy",
|
|
12235
|
-
observed: `Status=${state.Status} Health=${healthStatus || "no-check"}`
|
|
12236
|
-
};
|
|
12237
|
-
}
|
|
12238
|
-
return {
|
|
12239
|
-
ok: state.Status === until,
|
|
12240
|
-
observed: `Status=${state.Status}`
|
|
12241
|
-
};
|
|
12242
|
-
} catch (e) {
|
|
12243
|
-
return {
|
|
12244
|
-
ok: false,
|
|
12245
|
-
observed: "parse error: " + (e instanceof Error ? e.message : String(e))
|
|
12246
|
-
};
|
|
12247
|
-
}
|
|
12248
|
-
}
|
|
12249
|
-
const bucket = typeof a.bucket === "string" && a.bucket ? a.bucket : "";
|
|
12250
|
-
const path = String(a.target || a.path || "");
|
|
12251
|
-
if (!path) return { ok: false, observed: "no path" };
|
|
12252
|
-
if (bucket) {
|
|
12253
|
-
try {
|
|
12254
|
-
await getR2Client().send(
|
|
12255
|
-
new HeadObjectCommand({ Bucket: bucket, Key: r2Key(path) })
|
|
12256
|
-
);
|
|
12257
|
-
return { ok: true, observed: `r2://${bucket}/${path} exists` };
|
|
12258
|
-
} catch {
|
|
12259
|
-
return {
|
|
12260
|
-
ok: false,
|
|
12261
|
-
observed: `r2://${bucket}/${path} not found`
|
|
12262
|
-
};
|
|
12263
|
-
}
|
|
12264
|
-
}
|
|
12265
|
-
if (!a.serverId)
|
|
12266
|
-
return {
|
|
12267
|
-
ok: false,
|
|
12268
|
-
observed: "serverId or bucket is required for file-exists"
|
|
12269
|
-
};
|
|
12270
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
12271
|
-
const safe = sanitizePath(path);
|
|
12272
|
-
const cmd = `test -e ${posixQuote(safe)} && echo OK || echo MISSING`;
|
|
12273
|
-
const r = await sshExec(conn, cmd, proxy);
|
|
12274
|
-
const ok = r.stdout.trim() === "OK";
|
|
12275
|
-
return { ok, observed: ok ? `${safe} exists` : `${safe} missing` };
|
|
12276
|
-
};
|
|
12277
|
-
let attempts = 0;
|
|
12278
|
-
let lastObserved = "";
|
|
12279
|
-
while (Date.now() < deadline) {
|
|
12280
|
-
attempts++;
|
|
12281
|
-
const r = await check();
|
|
12282
|
-
lastObserved = r.observed;
|
|
12283
|
-
trail.push(
|
|
12284
|
-
`[${attempts}] ${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} ${r.ok ? "OK" : ".."} ${r.observed}`
|
|
12285
|
-
);
|
|
12286
|
-
if (r.ok) {
|
|
12287
|
-
return {
|
|
12288
|
-
content: [
|
|
12289
|
-
{
|
|
12290
|
-
type: "text",
|
|
12291
|
-
text: `Condition met after ${attempts} attempt(s) in ${((Date.now() - (deadline - timeoutSeconds * 1e3)) / 1e3).toFixed(1)}s.
|
|
12292
|
-
Last observation: ${r.observed}
|
|
12293
|
-
|
|
12294
|
-
--- trail ---
|
|
12295
|
-
${trail.join("\n")}`
|
|
12296
|
-
}
|
|
12297
|
-
]
|
|
12298
|
-
};
|
|
12299
|
-
}
|
|
12300
|
-
if (Date.now() + intervalSeconds * 1e3 >= deadline) break;
|
|
12301
|
-
await new Promise((res) => setTimeout(res, intervalSeconds * 1e3));
|
|
12302
|
-
}
|
|
12303
|
-
return {
|
|
12304
|
-
content: [
|
|
12305
|
-
{
|
|
12306
|
-
type: "text",
|
|
12307
|
-
text: `Timed out after ${timeoutSeconds}s without seeing condition (${attempts} probe(s)).
|
|
12308
|
-
Last observation: ${lastObserved}
|
|
12309
|
-
|
|
12310
|
-
--- trail ---
|
|
12311
|
-
${trail.join("\n")}`
|
|
12312
|
-
}
|
|
12313
|
-
]
|
|
12314
|
-
};
|
|
12315
|
-
}
|
|
12316
11606
|
// ----- Database -----
|
|
12317
11607
|
case "db-discover": {
|
|
12318
11608
|
const { serverId, conn, proxy } = await getServerConnection(
|
|
@@ -13188,53 +12478,6 @@ LIMIT ${limit};
|
|
|
13188
12478
|
}
|
|
13189
12479
|
return { content: [{ type: "text", text: saveMsg }] };
|
|
13190
12480
|
}
|
|
13191
|
-
case "release-trigger": {
|
|
13192
|
-
const releaseProfile = String(a.releaseProfile);
|
|
13193
|
-
const stageFilter = a.stage ? String(a.stage) : null;
|
|
13194
|
-
const versionType = String(a.versionType ?? "minor");
|
|
13195
|
-
if (versionType !== "major" && versionType !== "minor" && versionType !== "patch") {
|
|
13196
|
-
throw new Error(
|
|
13197
|
-
`Invalid versionType "${versionType}". Expected major, minor, or patch.`
|
|
13198
|
-
);
|
|
13199
|
-
}
|
|
13200
|
-
const skipPrFlow = a.skipPrFlow === true;
|
|
13201
|
-
const resolved = await resolveReleaseTriggerStage(
|
|
13202
|
-
releaseProfile,
|
|
13203
|
-
stageFilter
|
|
13204
|
-
);
|
|
13205
|
-
try {
|
|
13206
|
-
const result = await createReleaseForStage(authContext.userId, {
|
|
13207
|
-
stageId: resolved.stageId,
|
|
13208
|
-
versionType,
|
|
13209
|
-
skipPrFlow,
|
|
13210
|
-
triggerType: "mcp"
|
|
13211
|
-
});
|
|
13212
|
-
return {
|
|
13213
|
-
content: [
|
|
13214
|
-
{
|
|
13215
|
-
type: "text",
|
|
13216
|
-
text: JSON.stringify(
|
|
13217
|
-
{
|
|
13218
|
-
releaseId: result.id,
|
|
13219
|
-
status: result.status,
|
|
13220
|
-
stepsCount: result.stepsCount,
|
|
13221
|
-
author: result.authorName,
|
|
13222
|
-
profile: resolved.profileName,
|
|
13223
|
-
stage: resolved.stageName
|
|
13224
|
-
},
|
|
13225
|
-
null,
|
|
13226
|
-
2
|
|
13227
|
-
)
|
|
13228
|
-
}
|
|
13229
|
-
]
|
|
13230
|
-
};
|
|
13231
|
-
} catch (err) {
|
|
13232
|
-
if (err instanceof CreateReleaseError) {
|
|
13233
|
-
throw new Error(err.message);
|
|
13234
|
-
}
|
|
13235
|
-
throw err;
|
|
13236
|
-
}
|
|
13237
|
-
}
|
|
13238
12481
|
case "release-profile-list": {
|
|
13239
12482
|
const profiles = await db.execute(sql`
|
|
13240
12483
|
SELECT
|
|
@@ -13282,64 +12525,74 @@ ${lines.join("\n\n")}`
|
|
|
13282
12525
|
]
|
|
13283
12526
|
};
|
|
13284
12527
|
}
|
|
13285
|
-
case "release-profile
|
|
13286
|
-
const
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
if (releaseType !== null && releaseType !== "dev_only" && releaseType !== "prod_only" && releaseType !== "both") {
|
|
13290
|
-
throw new Error(
|
|
13291
|
-
`Invalid releaseType "${releaseType}". Expected dev_only, prod_only, or both.`
|
|
13292
|
-
);
|
|
12528
|
+
case "release-profile": {
|
|
12529
|
+
const action = String(a.action);
|
|
12530
|
+
if (action !== "create" && action !== "update") {
|
|
12531
|
+
throw new Error("action must be one of: create, update");
|
|
13293
12532
|
}
|
|
13294
|
-
|
|
13295
|
-
const
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
|
|
13304
|
-
|
|
13305
|
-
|
|
12533
|
+
if (action === "create") {
|
|
12534
|
+
const repoFullName = String(a.repoFullName ?? "").trim();
|
|
12535
|
+
if (!repoFullName) {
|
|
12536
|
+
throw new Error('action="create" requires repoFullName');
|
|
12537
|
+
}
|
|
12538
|
+
const githubTokenId2 = a.githubKey ? await resolveGitHubTokenIdByKey(String(a.githubKey)) : null;
|
|
12539
|
+
const releaseType2 = a.releaseType ? String(a.releaseType) : null;
|
|
12540
|
+
if (releaseType2 !== null && releaseType2 !== "dev_only" && releaseType2 !== "prod_only" && releaseType2 !== "both") {
|
|
12541
|
+
throw new Error(
|
|
12542
|
+
`Invalid releaseType "${releaseType2}". Expected dev_only, prod_only, or both.`
|
|
12543
|
+
);
|
|
12544
|
+
}
|
|
12545
|
+
try {
|
|
12546
|
+
const result = await quickCreateReleaseProfile(
|
|
12547
|
+
authContext.userId,
|
|
13306
12548
|
{
|
|
13307
|
-
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
name: result.profile.name,
|
|
13312
|
-
repo: repoFullName,
|
|
13313
|
-
releaseType: releaseType ?? "both",
|
|
13314
|
-
appsDetected: result.scan.apps.map((app) => ({
|
|
13315
|
-
path: app.path,
|
|
13316
|
-
label: app.label,
|
|
13317
|
-
deployMethod: app.deployMethod
|
|
13318
|
-
})),
|
|
13319
|
-
stages: result.stages.map((s) => ({
|
|
13320
|
-
stage: s.stage,
|
|
13321
|
-
name: s.name,
|
|
13322
|
-
releaseBranch: s.release_branch,
|
|
13323
|
-
triggerMode: s.trigger_mode
|
|
13324
|
-
})),
|
|
13325
|
-
branchWarnings: result.branchWarnings,
|
|
13326
|
-
hint: "Assign deployment servers per stage app in the dashboard (Releases \u2192 Profiles) before triggering a release."
|
|
13327
|
-
},
|
|
13328
|
-
null,
|
|
13329
|
-
2
|
|
13330
|
-
)
|
|
12549
|
+
repoFullName,
|
|
12550
|
+
githubTokenId: githubTokenId2,
|
|
12551
|
+
name: a.name ? String(a.name) : null,
|
|
12552
|
+
releaseType: releaseType2
|
|
13331
12553
|
}
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
12554
|
+
);
|
|
12555
|
+
return {
|
|
12556
|
+
content: [
|
|
12557
|
+
{
|
|
12558
|
+
type: "text",
|
|
12559
|
+
text: JSON.stringify(
|
|
12560
|
+
{
|
|
12561
|
+
profileId: result.profile.id,
|
|
12562
|
+
name: result.profile.name,
|
|
12563
|
+
repo: repoFullName,
|
|
12564
|
+
releaseType: releaseType2 ?? "both",
|
|
12565
|
+
appsDetected: result.scan.apps.map((app) => ({
|
|
12566
|
+
path: app.path,
|
|
12567
|
+
label: app.label,
|
|
12568
|
+
deployMethod: app.deployMethod
|
|
12569
|
+
})),
|
|
12570
|
+
stages: result.stages.map((s) => ({
|
|
12571
|
+
stage: s.stage,
|
|
12572
|
+
name: s.name,
|
|
12573
|
+
releaseBranch: s.release_branch,
|
|
12574
|
+
triggerMode: s.trigger_mode
|
|
12575
|
+
})),
|
|
12576
|
+
branchWarnings: result.branchWarnings,
|
|
12577
|
+
hint: "Assign deployment servers per stage app in the dashboard (Releases \u2192 Profiles) before triggering a release."
|
|
12578
|
+
},
|
|
12579
|
+
null,
|
|
12580
|
+
2
|
|
12581
|
+
)
|
|
12582
|
+
}
|
|
12583
|
+
]
|
|
12584
|
+
};
|
|
12585
|
+
} catch (err) {
|
|
12586
|
+
if (err instanceof ReleaseProfileQuickCreateError) {
|
|
12587
|
+
throw new Error(err.message);
|
|
12588
|
+
}
|
|
12589
|
+
throw err;
|
|
13337
12590
|
}
|
|
13338
|
-
throw err;
|
|
13339
12591
|
}
|
|
13340
|
-
}
|
|
13341
|
-
case "release-profile-update": {
|
|
13342
12592
|
const identifier = String(a.releaseProfile ?? "").trim();
|
|
12593
|
+
if (!identifier) {
|
|
12594
|
+
throw new Error('action="update" requires releaseProfile');
|
|
12595
|
+
}
|
|
13343
12596
|
const profileRows = await db.execute(sql`
|
|
13344
12597
|
SELECT id, name FROM release_profile
|
|
13345
12598
|
WHERE name ILIKE ${identifier} OR repo_full_name ILIKE ${identifier}
|