@mgsoftwarebv/mg-dashboard-mcp 7.0.9 → 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 +101 -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,
|
|
@@ -6976,7 +6419,6 @@ var TOOL_MODULE_MAP = {
|
|
|
6976
6419
|
"docker-logs": "ssh_servers",
|
|
6977
6420
|
"docker-exec": "ssh_servers",
|
|
6978
6421
|
"docker-compose": "ssh_servers",
|
|
6979
|
-
"wait-for": "ssh_servers",
|
|
6980
6422
|
"db-discover": "ssh_servers",
|
|
6981
6423
|
"db-tables": "ssh_servers",
|
|
6982
6424
|
"db-query": "ssh_servers",
|
|
@@ -6986,10 +6428,8 @@ var TOOL_MODULE_MAP = {
|
|
|
6986
6428
|
"env-list": "ci_cd",
|
|
6987
6429
|
"env-get": "ci_cd",
|
|
6988
6430
|
"env-store": "ci_cd",
|
|
6989
|
-
"release-trigger": "ci_cd",
|
|
6990
6431
|
"release-profile-list": "ci_cd",
|
|
6991
|
-
"release-profile
|
|
6992
|
-
"release-profile-update": "ci_cd",
|
|
6432
|
+
"release-profile": "ci_cd",
|
|
6993
6433
|
"domain-list": "domains",
|
|
6994
6434
|
"dns-list": "domains",
|
|
6995
6435
|
"dns-record": "domains",
|
|
@@ -9551,11 +8991,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
|
|
|
9551
8991
|
applied_by TEXT
|
|
9552
8992
|
);
|
|
9553
8993
|
`.trim();
|
|
9554
|
-
function normaliseMigrationSql(
|
|
9555
|
-
return
|
|
8994
|
+
function normaliseMigrationSql(sql23) {
|
|
8995
|
+
return sql23.replace(/\r\n/g, "\n").trim() + "\n";
|
|
9556
8996
|
}
|
|
9557
|
-
function migrationSha256(
|
|
9558
|
-
return createHash("sha256").update(normaliseMigrationSql(
|
|
8997
|
+
function migrationSha256(sql23) {
|
|
8998
|
+
return createHash("sha256").update(normaliseMigrationSql(sql23), "utf8").digest("hex");
|
|
9559
8999
|
}
|
|
9560
9000
|
function dollarQuoteTag(value) {
|
|
9561
9001
|
let tag = "_mcp";
|
|
@@ -10295,48 +9735,6 @@ var TOOLS = [
|
|
|
10295
9735
|
required: ["serverId", "projectPath", "action"]
|
|
10296
9736
|
}
|
|
10297
9737
|
},
|
|
10298
|
-
{
|
|
10299
|
-
name: "wait-for",
|
|
10300
|
-
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.',
|
|
10301
|
-
inputSchema: {
|
|
10302
|
-
type: "object",
|
|
10303
|
-
properties: {
|
|
10304
|
-
type: {
|
|
10305
|
-
type: "string",
|
|
10306
|
-
enum: ["url", "container-health", "file-exists", "sftp-exists"],
|
|
10307
|
-
description: "Condition type to wait on."
|
|
10308
|
-
},
|
|
10309
|
-
target: {
|
|
10310
|
-
type: "string",
|
|
10311
|
-
description: "Target identifier: URL, container name, file path, or R2 key (depends on type)."
|
|
10312
|
-
},
|
|
10313
|
-
until: {
|
|
10314
|
-
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.'
|
|
10315
|
-
},
|
|
10316
|
-
serverId: {
|
|
10317
|
-
type: "string",
|
|
10318
|
-
description: "SSH server UUID (required for container-health and file-exists)."
|
|
10319
|
-
},
|
|
10320
|
-
bucket: {
|
|
10321
|
-
type: "string",
|
|
10322
|
-
description: "R2 bucket name (used with sftp-exists when checking an R2 object)."
|
|
10323
|
-
},
|
|
10324
|
-
path: {
|
|
10325
|
-
type: "string",
|
|
10326
|
-
description: "Alias for `target` for file-exists/sftp-exists."
|
|
10327
|
-
},
|
|
10328
|
-
intervalSeconds: {
|
|
10329
|
-
type: "number",
|
|
10330
|
-
description: "Poll interval in seconds (default 3, max 60)."
|
|
10331
|
-
},
|
|
10332
|
-
timeout: {
|
|
10333
|
-
type: "number",
|
|
10334
|
-
description: "Total deadline in seconds (default 120, max 1800)."
|
|
10335
|
-
}
|
|
10336
|
-
},
|
|
10337
|
-
required: ["type", "target"]
|
|
10338
|
-
}
|
|
10339
|
-
},
|
|
10340
9738
|
{
|
|
10341
9739
|
name: "db-discover",
|
|
10342
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"] }`.',
|
|
@@ -10595,33 +9993,6 @@ var TOOLS = [
|
|
|
10595
9993
|
required: ["appName", "environment", "content"]
|
|
10596
9994
|
}
|
|
10597
9995
|
},
|
|
10598
|
-
{
|
|
10599
|
-
name: "release-trigger",
|
|
10600
|
-
description: "Start a release/deploy pipeline for a release profile stage. Attributes the deploy to the MCP API key owner. Requires ci_cd permission.",
|
|
10601
|
-
inputSchema: {
|
|
10602
|
-
type: "object",
|
|
10603
|
-
properties: {
|
|
10604
|
-
releaseProfile: {
|
|
10605
|
-
type: "string",
|
|
10606
|
-
description: "Release profile name (usually matches the project folder name or git repo name, e.g. mg-dashboard)."
|
|
10607
|
-
},
|
|
10608
|
-
stage: {
|
|
10609
|
-
type: "string",
|
|
10610
|
-
description: "Stage slug or name (e.g. dev, production). Required when the profile has multiple enabled stages."
|
|
10611
|
-
},
|
|
10612
|
-
versionType: {
|
|
10613
|
-
type: "string",
|
|
10614
|
-
enum: ["major", "minor", "patch"],
|
|
10615
|
-
description: "Semver bump type when versioning applies (default: minor)."
|
|
10616
|
-
},
|
|
10617
|
-
skipPrFlow: {
|
|
10618
|
-
type: "boolean",
|
|
10619
|
-
description: "Skip PR approval flow (default: false)."
|
|
10620
|
-
}
|
|
10621
|
-
},
|
|
10622
|
-
required: ["releaseProfile"]
|
|
10623
|
-
}
|
|
10624
|
-
},
|
|
10625
9996
|
{
|
|
10626
9997
|
name: "release-profile-list",
|
|
10627
9998
|
description: "List all release profiles with repo, enabled state, release type, GitHub key and a summary of their stages. Requires ci_cd permission.",
|
|
@@ -10632,67 +10003,55 @@ var TOOLS = [
|
|
|
10632
10003
|
}
|
|
10633
10004
|
},
|
|
10634
10005
|
{
|
|
10635
|
-
name: "release-profile
|
|
10636
|
-
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.',
|
|
10637
10008
|
inputSchema: {
|
|
10638
10009
|
type: "object",
|
|
10639
10010
|
properties: {
|
|
10011
|
+
action: {
|
|
10012
|
+
type: "string",
|
|
10013
|
+
enum: ["create", "update"],
|
|
10014
|
+
description: "Which operation to perform."
|
|
10015
|
+
},
|
|
10640
10016
|
repoFullName: {
|
|
10641
10017
|
type: "string",
|
|
10642
|
-
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".'
|
|
10643
10019
|
},
|
|
10644
|
-
|
|
10020
|
+
releaseProfile: {
|
|
10645
10021
|
type: "string",
|
|
10646
|
-
description: '
|
|
10022
|
+
description: 'Profile name or repo full name to locate the profile. Required for action="update".'
|
|
10647
10023
|
},
|
|
10648
10024
|
name: {
|
|
10649
10025
|
type: "string",
|
|
10650
|
-
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.'
|
|
10651
10027
|
},
|
|
10652
|
-
|
|
10653
|
-
type: "
|
|
10654
|
-
|
|
10655
|
-
description: "Which stages to create (default: both = dev + prod)."
|
|
10656
|
-
}
|
|
10657
|
-
},
|
|
10658
|
-
required: ["repoFullName"]
|
|
10659
|
-
}
|
|
10660
|
-
},
|
|
10661
|
-
{
|
|
10662
|
-
name: "release-profile-update",
|
|
10663
|
-
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.",
|
|
10664
|
-
inputSchema: {
|
|
10665
|
-
type: "object",
|
|
10666
|
-
properties: {
|
|
10667
|
-
releaseProfile: {
|
|
10668
|
-
type: "string",
|
|
10669
|
-
description: "Profile name or repo full name to locate the profile."
|
|
10028
|
+
enabled: {
|
|
10029
|
+
type: "boolean",
|
|
10030
|
+
description: "Enable/disable the profile (update only)."
|
|
10670
10031
|
},
|
|
10671
|
-
name: { type: "string", description: "New profile name." },
|
|
10672
|
-
enabled: { type: "boolean", description: "Enable/disable the profile." },
|
|
10673
10032
|
releaseType: {
|
|
10674
10033
|
type: "string",
|
|
10675
10034
|
enum: ["dev_only", "prod_only", "both"],
|
|
10676
|
-
description: "
|
|
10035
|
+
description: "Which stages exist (default for create: both = dev + prod). Changing it on update reconfigures the stages."
|
|
10677
10036
|
},
|
|
10678
10037
|
githubKey: {
|
|
10679
10038
|
type: "string",
|
|
10680
|
-
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.'
|
|
10681
10040
|
},
|
|
10682
10041
|
useChangesBranch: {
|
|
10683
10042
|
type: "boolean",
|
|
10684
|
-
description: "Use a long-lived -changes accumulation branch."
|
|
10043
|
+
description: "Use a long-lived -changes accumulation branch (update only)."
|
|
10685
10044
|
},
|
|
10686
10045
|
hasTriggerDev: {
|
|
10687
10046
|
type: "boolean",
|
|
10688
|
-
description: "Deploy Trigger.dev jobs during production releases."
|
|
10047
|
+
description: "Deploy Trigger.dev jobs during production releases (update only)."
|
|
10689
10048
|
},
|
|
10690
10049
|
workDirectory: {
|
|
10691
10050
|
type: "string",
|
|
10692
|
-
description: 'Working directory within the repo (e.g. ".").'
|
|
10051
|
+
description: 'Working directory within the repo (e.g. "."). Update only.'
|
|
10693
10052
|
}
|
|
10694
10053
|
},
|
|
10695
|
-
required: ["
|
|
10054
|
+
required: ["action"]
|
|
10696
10055
|
}
|
|
10697
10056
|
},
|
|
10698
10057
|
{
|
|
@@ -12244,163 +11603,6 @@ ${stderrOut}`);
|
|
|
12244
11603
|
${result.stderr}`);
|
|
12245
11604
|
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
12246
11605
|
}
|
|
12247
|
-
// ----- wait-for: poll until a condition holds (one tool, four types) -----
|
|
12248
|
-
case "wait-for": {
|
|
12249
|
-
const type = String(a.type || "");
|
|
12250
|
-
if (!["url", "container-health", "file-exists", "sftp-exists"].includes(
|
|
12251
|
-
type
|
|
12252
|
-
)) {
|
|
12253
|
-
return {
|
|
12254
|
-
content: [
|
|
12255
|
-
{
|
|
12256
|
-
type: "text",
|
|
12257
|
-
text: "Error: type must be one of: url, container-health, file-exists, sftp-exists"
|
|
12258
|
-
}
|
|
12259
|
-
]
|
|
12260
|
-
};
|
|
12261
|
-
}
|
|
12262
|
-
const timeoutSeconds = Math.max(
|
|
12263
|
-
1,
|
|
12264
|
-
Math.min(1800, Number(a.timeout) || 120)
|
|
12265
|
-
);
|
|
12266
|
-
const intervalSeconds = Math.max(
|
|
12267
|
-
1,
|
|
12268
|
-
Math.min(60, Number(a.intervalSeconds) || 3)
|
|
12269
|
-
);
|
|
12270
|
-
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
12271
|
-
const trail = [];
|
|
12272
|
-
const check = async () => {
|
|
12273
|
-
if (type === "url") {
|
|
12274
|
-
const target = String(a.target || "");
|
|
12275
|
-
if (!target) return { ok: false, observed: "no target url" };
|
|
12276
|
-
const expectStatus = a.until !== void 0 ? Number(a.until) : 200;
|
|
12277
|
-
try {
|
|
12278
|
-
const ctrl = new AbortController();
|
|
12279
|
-
const t = setTimeout(() => ctrl.abort(), 1e4);
|
|
12280
|
-
const res = await fetch(target, {
|
|
12281
|
-
signal: ctrl.signal,
|
|
12282
|
-
redirect: "manual"
|
|
12283
|
-
});
|
|
12284
|
-
clearTimeout(t);
|
|
12285
|
-
const ok2 = res.status === expectStatus || expectStatus === 200 && res.status >= 200 && res.status < 300;
|
|
12286
|
-
return { ok: ok2, observed: `HTTP ${res.status}` };
|
|
12287
|
-
} catch (e) {
|
|
12288
|
-
return {
|
|
12289
|
-
ok: false,
|
|
12290
|
-
observed: e instanceof Error ? e.message : String(e)
|
|
12291
|
-
};
|
|
12292
|
-
}
|
|
12293
|
-
}
|
|
12294
|
-
if (type === "container-health") {
|
|
12295
|
-
const containerName = String(a.target || "").replace(
|
|
12296
|
-
/[^a-zA-Z0-9._-]/g,
|
|
12297
|
-
""
|
|
12298
|
-
);
|
|
12299
|
-
const until = String(a.until || "healthy");
|
|
12300
|
-
if (!containerName)
|
|
12301
|
-
return { ok: false, observed: "no container name" };
|
|
12302
|
-
if (!a.serverId)
|
|
12303
|
-
return {
|
|
12304
|
-
ok: false,
|
|
12305
|
-
observed: "serverId is required for container-health"
|
|
12306
|
-
};
|
|
12307
|
-
const { conn: conn2, proxy: proxy2 } = await getServerConnection(
|
|
12308
|
-
String(a.serverId)
|
|
12309
|
-
);
|
|
12310
|
-
const cmd2 = `docker inspect --format '{{json .State}}' ${containerName} 2>&1`;
|
|
12311
|
-
const r2 = await sshExec(conn2, cmd2, proxy2);
|
|
12312
|
-
if (r2.exitCode !== 0)
|
|
12313
|
-
return {
|
|
12314
|
-
ok: false,
|
|
12315
|
-
observed: r2.stderr.trim().slice(0, 80) || "inspect failed"
|
|
12316
|
-
};
|
|
12317
|
-
try {
|
|
12318
|
-
const state = JSON.parse(r2.stdout.trim());
|
|
12319
|
-
const healthStatus = state.Health?.Status;
|
|
12320
|
-
if (until === "healthy") {
|
|
12321
|
-
return {
|
|
12322
|
-
ok: healthStatus === "healthy",
|
|
12323
|
-
observed: `Status=${state.Status} Health=${healthStatus || "no-check"}`
|
|
12324
|
-
};
|
|
12325
|
-
}
|
|
12326
|
-
return {
|
|
12327
|
-
ok: state.Status === until,
|
|
12328
|
-
observed: `Status=${state.Status}`
|
|
12329
|
-
};
|
|
12330
|
-
} catch (e) {
|
|
12331
|
-
return {
|
|
12332
|
-
ok: false,
|
|
12333
|
-
observed: "parse error: " + (e instanceof Error ? e.message : String(e))
|
|
12334
|
-
};
|
|
12335
|
-
}
|
|
12336
|
-
}
|
|
12337
|
-
const bucket = typeof a.bucket === "string" && a.bucket ? a.bucket : "";
|
|
12338
|
-
const path = String(a.target || a.path || "");
|
|
12339
|
-
if (!path) return { ok: false, observed: "no path" };
|
|
12340
|
-
if (bucket) {
|
|
12341
|
-
try {
|
|
12342
|
-
await getR2Client().send(
|
|
12343
|
-
new HeadObjectCommand({ Bucket: bucket, Key: r2Key(path) })
|
|
12344
|
-
);
|
|
12345
|
-
return { ok: true, observed: `r2://${bucket}/${path} exists` };
|
|
12346
|
-
} catch {
|
|
12347
|
-
return {
|
|
12348
|
-
ok: false,
|
|
12349
|
-
observed: `r2://${bucket}/${path} not found`
|
|
12350
|
-
};
|
|
12351
|
-
}
|
|
12352
|
-
}
|
|
12353
|
-
if (!a.serverId)
|
|
12354
|
-
return {
|
|
12355
|
-
ok: false,
|
|
12356
|
-
observed: "serverId or bucket is required for file-exists"
|
|
12357
|
-
};
|
|
12358
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
12359
|
-
const safe = sanitizePath(path);
|
|
12360
|
-
const cmd = `test -e ${posixQuote(safe)} && echo OK || echo MISSING`;
|
|
12361
|
-
const r = await sshExec(conn, cmd, proxy);
|
|
12362
|
-
const ok = r.stdout.trim() === "OK";
|
|
12363
|
-
return { ok, observed: ok ? `${safe} exists` : `${safe} missing` };
|
|
12364
|
-
};
|
|
12365
|
-
let attempts = 0;
|
|
12366
|
-
let lastObserved = "";
|
|
12367
|
-
while (Date.now() < deadline) {
|
|
12368
|
-
attempts++;
|
|
12369
|
-
const r = await check();
|
|
12370
|
-
lastObserved = r.observed;
|
|
12371
|
-
trail.push(
|
|
12372
|
-
`[${attempts}] ${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} ${r.ok ? "OK" : ".."} ${r.observed}`
|
|
12373
|
-
);
|
|
12374
|
-
if (r.ok) {
|
|
12375
|
-
return {
|
|
12376
|
-
content: [
|
|
12377
|
-
{
|
|
12378
|
-
type: "text",
|
|
12379
|
-
text: `Condition met after ${attempts} attempt(s) in ${((Date.now() - (deadline - timeoutSeconds * 1e3)) / 1e3).toFixed(1)}s.
|
|
12380
|
-
Last observation: ${r.observed}
|
|
12381
|
-
|
|
12382
|
-
--- trail ---
|
|
12383
|
-
${trail.join("\n")}`
|
|
12384
|
-
}
|
|
12385
|
-
]
|
|
12386
|
-
};
|
|
12387
|
-
}
|
|
12388
|
-
if (Date.now() + intervalSeconds * 1e3 >= deadline) break;
|
|
12389
|
-
await new Promise((res) => setTimeout(res, intervalSeconds * 1e3));
|
|
12390
|
-
}
|
|
12391
|
-
return {
|
|
12392
|
-
content: [
|
|
12393
|
-
{
|
|
12394
|
-
type: "text",
|
|
12395
|
-
text: `Timed out after ${timeoutSeconds}s without seeing condition (${attempts} probe(s)).
|
|
12396
|
-
Last observation: ${lastObserved}
|
|
12397
|
-
|
|
12398
|
-
--- trail ---
|
|
12399
|
-
${trail.join("\n")}`
|
|
12400
|
-
}
|
|
12401
|
-
]
|
|
12402
|
-
};
|
|
12403
|
-
}
|
|
12404
11606
|
// ----- Database -----
|
|
12405
11607
|
case "db-discover": {
|
|
12406
11608
|
const { serverId, conn, proxy } = await getServerConnection(
|
|
@@ -13276,53 +12478,6 @@ LIMIT ${limit};
|
|
|
13276
12478
|
}
|
|
13277
12479
|
return { content: [{ type: "text", text: saveMsg }] };
|
|
13278
12480
|
}
|
|
13279
|
-
case "release-trigger": {
|
|
13280
|
-
const releaseProfile = String(a.releaseProfile);
|
|
13281
|
-
const stageFilter = a.stage ? String(a.stage) : null;
|
|
13282
|
-
const versionType = String(a.versionType ?? "minor");
|
|
13283
|
-
if (versionType !== "major" && versionType !== "minor" && versionType !== "patch") {
|
|
13284
|
-
throw new Error(
|
|
13285
|
-
`Invalid versionType "${versionType}". Expected major, minor, or patch.`
|
|
13286
|
-
);
|
|
13287
|
-
}
|
|
13288
|
-
const skipPrFlow = a.skipPrFlow === true;
|
|
13289
|
-
const resolved = await resolveReleaseTriggerStage(
|
|
13290
|
-
releaseProfile,
|
|
13291
|
-
stageFilter
|
|
13292
|
-
);
|
|
13293
|
-
try {
|
|
13294
|
-
const result = await createReleaseForStage(authContext.userId, {
|
|
13295
|
-
stageId: resolved.stageId,
|
|
13296
|
-
versionType,
|
|
13297
|
-
skipPrFlow,
|
|
13298
|
-
triggerType: "mcp"
|
|
13299
|
-
});
|
|
13300
|
-
return {
|
|
13301
|
-
content: [
|
|
13302
|
-
{
|
|
13303
|
-
type: "text",
|
|
13304
|
-
text: JSON.stringify(
|
|
13305
|
-
{
|
|
13306
|
-
releaseId: result.id,
|
|
13307
|
-
status: result.status,
|
|
13308
|
-
stepsCount: result.stepsCount,
|
|
13309
|
-
author: result.authorName,
|
|
13310
|
-
profile: resolved.profileName,
|
|
13311
|
-
stage: resolved.stageName
|
|
13312
|
-
},
|
|
13313
|
-
null,
|
|
13314
|
-
2
|
|
13315
|
-
)
|
|
13316
|
-
}
|
|
13317
|
-
]
|
|
13318
|
-
};
|
|
13319
|
-
} catch (err) {
|
|
13320
|
-
if (err instanceof CreateReleaseError) {
|
|
13321
|
-
throw new Error(err.message);
|
|
13322
|
-
}
|
|
13323
|
-
throw err;
|
|
13324
|
-
}
|
|
13325
|
-
}
|
|
13326
12481
|
case "release-profile-list": {
|
|
13327
12482
|
const profiles = await db.execute(sql`
|
|
13328
12483
|
SELECT
|
|
@@ -13370,64 +12525,74 @@ ${lines.join("\n\n")}`
|
|
|
13370
12525
|
]
|
|
13371
12526
|
};
|
|
13372
12527
|
}
|
|
13373
|
-
case "release-profile
|
|
13374
|
-
const
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
if (releaseType !== null && releaseType !== "dev_only" && releaseType !== "prod_only" && releaseType !== "both") {
|
|
13378
|
-
throw new Error(
|
|
13379
|
-
`Invalid releaseType "${releaseType}". Expected dev_only, prod_only, or both.`
|
|
13380
|
-
);
|
|
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");
|
|
13381
12532
|
}
|
|
13382
|
-
|
|
13383
|
-
const
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
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,
|
|
13394
12548
|
{
|
|
13395
|
-
|
|
13396
|
-
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
name: result.profile.name,
|
|
13400
|
-
repo: repoFullName,
|
|
13401
|
-
releaseType: releaseType ?? "both",
|
|
13402
|
-
appsDetected: result.scan.apps.map((app) => ({
|
|
13403
|
-
path: app.path,
|
|
13404
|
-
label: app.label,
|
|
13405
|
-
deployMethod: app.deployMethod
|
|
13406
|
-
})),
|
|
13407
|
-
stages: result.stages.map((s) => ({
|
|
13408
|
-
stage: s.stage,
|
|
13409
|
-
name: s.name,
|
|
13410
|
-
releaseBranch: s.release_branch,
|
|
13411
|
-
triggerMode: s.trigger_mode
|
|
13412
|
-
})),
|
|
13413
|
-
branchWarnings: result.branchWarnings,
|
|
13414
|
-
hint: "Assign deployment servers per stage app in the dashboard (Releases \u2192 Profiles) before triggering a release."
|
|
13415
|
-
},
|
|
13416
|
-
null,
|
|
13417
|
-
2
|
|
13418
|
-
)
|
|
12549
|
+
repoFullName,
|
|
12550
|
+
githubTokenId: githubTokenId2,
|
|
12551
|
+
name: a.name ? String(a.name) : null,
|
|
12552
|
+
releaseType: releaseType2
|
|
13419
12553
|
}
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
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;
|
|
13425
12590
|
}
|
|
13426
|
-
throw err;
|
|
13427
12591
|
}
|
|
13428
|
-
}
|
|
13429
|
-
case "release-profile-update": {
|
|
13430
12592
|
const identifier = String(a.releaseProfile ?? "").trim();
|
|
12593
|
+
if (!identifier) {
|
|
12594
|
+
throw new Error('action="update" requires releaseProfile');
|
|
12595
|
+
}
|
|
13431
12596
|
const profileRows = await db.execute(sql`
|
|
13432
12597
|
SELECT id, name FROM release_profile
|
|
13433
12598
|
WHERE name ILIKE ${identifier} OR repo_full_name ILIKE ${identifier}
|