presenceValidator.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import { createRequire } from "node:module";
  2. import { existsSync, readFileSync } from "node:fs";
  3. import { resolve } from "node:path";
  4. import actions from "@actions/core";
  5. import got from "got";
  6. import jsonAst, {
  7. ObjectNode,
  8. ArrayNode,
  9. LiteralNode,
  10. PropertyNode,
  11. } from "json-to-ast";
  12. import { validate } from "jsonschema";
  13. import { compare } from "semver";
  14. import PresenceCompiler from "../classes/PresenceCompiler.js";
  15. import { getDiff, getLatestSchema } from "../util.js";
  16. import chalk from "chalk";
  17. interface SchemaMetadata extends Metadata {
  18. $schema: string;
  19. }
  20. type ValueNode = ObjectNode | ArrayNode | LiteralNode | PropertyNode;
  21. const require = createRequire(import.meta.url),
  22. schema = await getLatestSchema(),
  23. changedPresences = getDiff(),
  24. validLangs = await getValidLanguages(),
  25. compiler = new PresenceCompiler(),
  26. errors: {
  27. presence: string;
  28. message: string | Error;
  29. properties?: actions.AnnotationProperties | undefined;
  30. }[] = [],
  31. warnings: {
  32. presence: string;
  33. message: string | Error;
  34. properties?: actions.AnnotationProperties | undefined;
  35. }[] = [];
  36. for (const presence of changedPresences) {
  37. const presencePath = compiler.getPresenceFolder(presence);
  38. //#region Metadata Check
  39. if (!existsSync(resolve(presencePath, "metadata.json"))) {
  40. errors.push({
  41. presence,
  42. message: `Presence (${presence}) is missing metadata.json`,
  43. properties: {
  44. file: resolve(presencePath, "metadata.json"),
  45. },
  46. });
  47. continue;
  48. }
  49. let metadata: SchemaMetadata;
  50. try {
  51. metadata = require(resolve(presencePath, "metadata.json"));
  52. } catch {
  53. errors.push({
  54. presence,
  55. message: `Presence (${presence}) metadata.json is not a valid JSON file`,
  56. properties: {
  57. file: resolve(presencePath, "metadata.json"),
  58. },
  59. });
  60. continue;
  61. }
  62. //#endregion
  63. const storePresence = (
  64. await getStorePresence(metadata.service)
  65. ).data.presences.find(p => p.metadata.service === metadata.service);
  66. //#region Schema Check
  67. const result = validate(metadata, schema.schema);
  68. if (result.errors.length) {
  69. for (const error of result.errors) {
  70. errors.push({
  71. presence,
  72. message: error.message,
  73. properties: {
  74. file: resolve(presencePath, "metadata.json"),
  75. startLine: getLine(...error.path),
  76. },
  77. });
  78. }
  79. continue;
  80. }
  81. //#endregion
  82. //#region Schema version Check
  83. if (metadata.$schema !== schema.url) {
  84. errors.push({
  85. presence,
  86. message: `Schema version is not up to date (Presence: ${presence}) - expected: ${schema.url}, got: ${metadata.$schema}`,
  87. properties: {
  88. file: resolve(presencePath, "metadata.json"),
  89. startLine: getLine("$schema"),
  90. },
  91. });
  92. continue;
  93. }
  94. //#endregion
  95. //#region Version bump Check
  96. if (storePresence) {
  97. if (compare(metadata.version, storePresence.metadata.version) <= 0) {
  98. errors.push({
  99. presence,
  100. message: `Version has not been bumped (Presence: ${presence})`,
  101. properties: {
  102. file: resolve(presencePath, "metadata.json"),
  103. startLine: getLine("version"),
  104. },
  105. });
  106. }
  107. } else if (metadata.version !== "1.0.0") {
  108. errors.push({
  109. presence,
  110. message: `Initial version must be 1.0.0 (Presence: ${presence})`,
  111. properties: {
  112. file: resolve(presencePath, "metadata.json"),
  113. startLine: getLine("version"),
  114. },
  115. });
  116. }
  117. //#endregion
  118. //#region Presence iFrame Check
  119. const iframePath = resolve(presencePath, "iframe.ts");
  120. if (!existsSync(iframePath) && metadata.iframe) {
  121. errors.push({
  122. presence,
  123. message: `Presence (${presence}) is missing iframe.ts`,
  124. properties: {
  125. file: iframePath,
  126. },
  127. });
  128. }
  129. if (!metadata.iframe && existsSync(iframePath)) {
  130. errors.push({
  131. presence,
  132. message: `Presence (${presence}) has iframe.ts but metadata.iframe is set to false`,
  133. properties: {
  134. file: iframePath,
  135. },
  136. });
  137. }
  138. if (metadata.iFrameRegExp === ".*") {
  139. warnings.push({
  140. presence,
  141. message: `Presence (${presence}) has metadata.iFrameRegExp set to '.*', please change this if possible`,
  142. properties: {
  143. file: resolve(presencePath, "metadata.json"),
  144. startLine: getLine("iFrameRegExp"),
  145. },
  146. });
  147. }
  148. if (metadata.iFrameRegExp && !metadata.iframe) {
  149. errors.push({
  150. presence,
  151. message: `Presence (${presence}) has metadata.iFrameRegExp set but metadata.iframe is set to false`,
  152. properties: {
  153. file: resolve(presencePath, "metadata.json"),
  154. startLine: getLine("iFrameRegExp"),
  155. },
  156. });
  157. }
  158. if (!metadata.iFrameRegExp && metadata.iframe) {
  159. warnings.push({
  160. presence,
  161. message: `Presence (${presence}) has metadata.iframe set to true but metadata.iFrameRegExp is not set, you may want to set it`,
  162. properties: {
  163. file: resolve(presencePath, "metadata.json"),
  164. startLine: getLine("iFrameRegExp"),
  165. },
  166. });
  167. }
  168. //#endregion
  169. //#region Presence language Check
  170. Object.keys(metadata.description).forEach(lang => {
  171. const index = validLangs.findIndex((l: string) => l === lang);
  172. if (!~index) {
  173. errors.push({
  174. presence,
  175. message: `Language ${lang} is not supported`,
  176. properties: {
  177. file: resolve(presencePath, "metadata.json"),
  178. startLine: getLine("description", lang),
  179. },
  180. });
  181. }
  182. });
  183. //#endregion
  184. function getLine(...path: (string | number)[]): number | undefined {
  185. const AST = jsonAst(
  186. readFileSync(resolve(presencePath, "metadata.json"), "utf-8"),
  187. {
  188. source: resolve(presencePath, "metadata.json"),
  189. }
  190. ) as ObjectNode;
  191. let currentNode: ValueNode | undefined = AST.children.find(
  192. x => x.key.value === path[0]
  193. ),
  194. isRoot = true;
  195. for (const value of path) {
  196. if (isRoot) {
  197. isRoot = false;
  198. continue;
  199. }
  200. if (!currentNode) return 0;
  201. else currentNode = findNodeLine(currentNode, value) as PropertyNode;
  202. }
  203. return currentNode?.loc?.start.line;
  204. }
  205. function findNodeLine(
  206. node: ValueNode,
  207. value: string | number
  208. ): ValueNode | undefined {
  209. switch (node.type) {
  210. case "Property":
  211. return findNodeLine(node.value, value);
  212. case "Array":
  213. if (Number.isInteger(value)) return node.children[value as number];
  214. else return node.children.find(x => findNodeLine(x, value));
  215. case "Object":
  216. return node.children.find(x => x.key.value === value);
  217. case "Literal":
  218. return node;
  219. }
  220. }
  221. actions.info(chalk.green(`${metadata.service} validated successfully`));
  222. }
  223. if (warnings.length)
  224. for (const warning of warnings)
  225. actions.warning(warning.message, warning.properties);
  226. if (errors.length) {
  227. for (const error of errors) actions.error(error.message, error.properties);
  228. actions.setFailed("Some Presences failed to validate.");
  229. } else actions.info("All Presences validated successfully");
  230. async function getValidLanguages() {
  231. return (
  232. await got<{
  233. data: {
  234. langFiles: [
  235. {
  236. lang: string;
  237. }
  238. ];
  239. };
  240. }>("https://api.premid.app/v3", {
  241. method: "post",
  242. responseType: "json",
  243. headers: {
  244. "Content-Type": "application/json",
  245. },
  246. body: JSON.stringify({
  247. query: `
  248. query {
  249. langFiles(project: "presence") {
  250. lang
  251. }
  252. }
  253. `,
  254. }),
  255. })
  256. ).body.data.langFiles.map(l => l.lang);
  257. }
  258. async function getStorePresence(presences: string) {
  259. try {
  260. return (
  261. await got<{
  262. data: {
  263. presences: [
  264. {
  265. metadata: {
  266. service: string;
  267. version: string;
  268. };
  269. }
  270. ];
  271. };
  272. }>("https://api.premid.app/v3", {
  273. method: "post",
  274. responseType: "json",
  275. headers: {
  276. "Content-Type": "application/json",
  277. },
  278. body: JSON.stringify({
  279. query: `
  280. query getData($service: StringOrStringArray!) {
  281. presences(service: $service) {
  282. metadata {
  283. service
  284. version
  285. }
  286. }
  287. }
  288. `,
  289. variables: {
  290. service: presences,
  291. },
  292. }),
  293. })
  294. ).body;
  295. } catch {
  296. actions.setFailed("Could not fetch store data");
  297. process.exit();
  298. }
  299. }