diff --git a/.github/scripts/.gitignore b/.github/scripts/.gitignore new file mode 100644 index 000000000..a14702c40 --- /dev/null +++ b/.github/scripts/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.github/scripts/README.md b/.github/scripts/README.md new file mode 100644 index 000000000..b352245b1 --- /dev/null +++ b/.github/scripts/README.md @@ -0,0 +1,15 @@ +# scripts + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run +``` + +This project was created using `bun init` in bun v1.2.11. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/.github/scripts/bun.lock b/.github/scripts/bun.lock new file mode 100644 index 000000000..dfb1f80e8 --- /dev/null +++ b/.github/scripts/bun.lock @@ -0,0 +1,106 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "scripts", + "dependencies": { + "@actions/core": "^1.11.1", + "@actions/github": "^6.0.0", + "@tcgdex/sdk": "^2.6.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="], + + "@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="], + + "@actions/github": ["@actions/github@6.0.0", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.0.0", "@octokit/plugin-rest-endpoint-methods": "^10.0.0" } }, "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g=="], + + "@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], + + "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], + + "@cachex/core": ["@cachex/core@1.0.1", "", { "dependencies": { "@dzeio/object-util": "^1.8.3" } }, "sha512-sHynwjF9hKIvwg8rTUdvA7MOUMcUC5Mq0dpynPBILRS+IPvsHcE4Cb2uRSs0/I2nxO7NQp9p+xHYistdfJJSwg=="], + + "@cachex/memory": ["@cachex/memory@1.0.1", "", { "dependencies": { "@cachex/core": "^1" } }, "sha512-KWUTdCCXhIlAkJaVZMhUh9kD0uq8PxC3Z34Q3lMGZOAV6FXP/cOMW89ALrWX3VkoRrrM4R6MIMO+amZNOvEqgw=="], + + "@cachex/web-storage": ["@cachex/web-storage@1.0.1", "", { "dependencies": { "@cachex/core": "^1" } }, "sha512-E8Xa9qDZgNgr+lcj3eixowg7PH2CVZbp3huuoc5xVVTtwYrZi5YqbHBG12yG3r6C6Fts/2Yoq6cbVBSm6c8VRA=="], + + "@dzeio/object-util": ["@dzeio/object-util@1.9.1", "", {}, "sha512-cLGsjAc7hzSadS57jcMxSPidYabyZXJOFnasScSrE/V5yflhze6T7L5/98josWYrXMvoKu7N+Ivk6vGkIj72UQ=="], + + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + + "@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], + + "@octokit/core": ["@octokit/core@5.2.1", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ=="], + + "@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="], + + "@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], + + "@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="], + + "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="], + + "@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="], + + "@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="], + + "@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + + "@tcgdex/sdk": ["@tcgdex/sdk@2.6.0", "", { "dependencies": { "@cachex/memory": "^1", "@cachex/web-storage": "^1", "@dzeio/object-util": "^1", "isomorphic-unfetch": "^3" } }, "sha512-q0O7dNzRRLq38XwqUoHFKo78/NCQW5pEIZ+JnlrVOxp9r9R/LTIQGAMNR96Il0hb1uIm4j5o7WBueBCpuDyomQ=="], + + "@types/bun": ["@types/bun@1.2.11", "", { "dependencies": { "bun-types": "1.2.11" } }, "sha512-ZLbbI91EmmGwlWTRWuV6J19IUiUC5YQ3TCEuSHI3usIP75kuoA8/0PVF+LTrbEnVc8JIhpElWOxv1ocI1fJBbw=="], + + "@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="], + + "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], + + "bun-types": ["bun-types@1.2.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-dbkp5Lo8HDrXkLrONm6bk+yiiYQSntvFUzQp0v3pzTAsXk6FtgVMjdQ+lzFNVAmQFUkPQZ3WMZqH5tTo+Dp/IA=="], + + "deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="], + + "isomorphic-unfetch": ["isomorphic-unfetch@3.1.0", "", { "dependencies": { "node-fetch": "^2.6.1", "unfetch": "^4.2.0" } }, "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unfetch": ["unfetch@4.2.0", "", {}, "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="], + + "universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + } +} diff --git a/.github/scripts/load-cards.ts b/.github/scripts/load-cards.ts new file mode 100644 index 000000000..e3b1603f0 --- /dev/null +++ b/.github/scripts/load-cards.ts @@ -0,0 +1,371 @@ +import core from "@actions/core"; +import github from "@actions/github"; +import TCGdex from "@tcgdex/sdk"; + +// Types +type CardData = { + id: string; + name: string; + image?: string; + rarity?: string; + set: { name: string }; + hasImage: boolean; +}; + +type CardResult = { + file: string; + card?: CardData; + error?: string; + isAsian?: boolean; + usedLanguage?: string; + hasImage?: boolean; +}; + +type CardFetchResult = { + card: any; + usedLanguage: string; + hasImage: boolean; +} | null; + +// Constants +const DATA_REGEX = /^data\/([^\/]+)\/([^\/]+)\/([^\/]+)\.ts$/; +const DATA_ASIA_REGEX = /^data-asia\/([^\/]+)\/([^\/]+)\/([^\/]+)\.ts$/; + +const INTERNATIONAL_LANGUAGES = ["en", "fr", "es", "es-mx", "it", "pt", "pt-br", "pt-pt", "de", "nl", "pl", "ru"]; +const ASIAN_LANGUAGES = ["ja", "ko", "zh-tw", "id", "th", "zh-cn"]; + +const LANGUAGE_NAMES: Record = { + en: "English", + fr: "French", + es: "Spanish", + "es-mx": "Spanish (Mexico)", + it: "Italian", + pt: "Portuguese", + "pt-br": "Portuguese (Brazil)", + "pt-pt": "Portuguese (Portugal)", + de: "German", + nl: "Dutch", + pl: "Polish", + ru: "Russian", + ja: "Japanese", + ko: "Korean", + "zh-tw": "Chinese (Taiwan)", + id: "Indonesian", + th: "Thai", + "zh-cn": "Chinese (China)", +}; + +// Helper function to sanitize card data +function sanitizeCardData(card: any): CardData | undefined { + if (!card) return undefined; + + return { + id: card.id, + name: card.name, + image: card.image, + rarity: card.rarity, + set: card.set ? { name: card.set.name } : { name: "Unknown" }, + hasImage: !!card.image, + }; +} + +// Helper function to try fetching a card with fallback to other languages +async function tryFetchCardWithFallback( + setIdentifier: string, + cardLocalId: string, + primaryLanguage: string, + allLanguages: string[], + isAsianRegion: boolean, +): Promise { + let lastError: Error | unknown = null; + const languagesToTry = [primaryLanguage, ...allLanguages.filter((lang) => lang !== primaryLanguage)]; + let foundWithoutImage: { lang: string; card: any } | undefined; + + for (const lang of languagesToTry) { + try { + console.log(` Trying language: ${lang}`); + const tcgdex = new TCGdex(lang as any); + let card; + + if (!isAsianRegion) { + const set = await tcgdex.set.get(setIdentifier); + card = await tcgdex.card.get(`${set!.id}-${cardLocalId}`); + } else { + card = await tcgdex.card.get(`${setIdentifier}-${cardLocalId}`); + } + + if (card && !card.image) { + foundWithoutImage = { lang, card }; + continue; + } + + if (card && card.image) { + console.log(` Card: ${card.name} (${card.id}) - Found using language: ${lang}`); + return { card, usedLanguage: lang, hasImage: true }; + } + } catch (error) { + lastError = error; + console.log(` Failed with language ${lang}: ${error instanceof Error ? error.message : "Unknown error"}`); + } + } + + if (foundWithoutImage) { + console.log(` Card: ${foundWithoutImage.card.name} (${foundWithoutImage.card.id}) - Found without image`); + return { card: foundWithoutImage.card, usedLanguage: foundWithoutImage.lang, hasImage: false }; + } + + console.log( + ` All languages failed. Last error: ${lastError instanceof Error ? lastError.message : "Unknown error"}`, + ); + return null; +} + +// Get changed files from GitHub +async function getChangedFiles( + context: typeof github.context, + octokit: ReturnType, +): Promise { + if (context.payload.pull_request) { + const { owner, repo } = context.repo; + const prNumber = context.payload.pull_request.number; + const response = await octokit.rest.pulls.listFiles({ + owner, + repo, + pull_number: prNumber, + }); + return response.data.map((file) => file.filename); + } else if (context.payload.commits) { + const filesSet = new Set(); + for (const commit of context.payload.commits) { + ["added", "modified", "removed"].forEach((type) => { + if (commit[type]) commit[type].forEach((file: string) => filesSet.add(file)); + }); + } + return Array.from(filesSet); + } + return []; +} + +// Process a single card file +async function processCardFile(file: string): Promise { + console.log(` - ${file}`); + let match = file.match(DATA_REGEX); + + if (match) { + const [_, , setName, cardLocalId] = match; + const result = await tryFetchCardWithFallback(setName!, cardLocalId!, "en", INTERNATIONAL_LANGUAGES, false); + + if (result) { + return { + file, + card: sanitizeCardData(result.card), + isAsian: false, + usedLanguage: result.usedLanguage, + hasImage: result.hasImage, + }; + } else { + return { + file, + error: "Failed to fetch card information in all available languages", + isAsian: false, + }; + } + } + + match = file.match(DATA_ASIA_REGEX); + if (match) { + const [_, , setId, cardLocalId] = match; + const result = await tryFetchCardWithFallback(setId!, cardLocalId!, "ja", ASIAN_LANGUAGES, true); + + if (result) { + return { + file, + card: sanitizeCardData(result.card), + isAsian: true, + usedLanguage: result.usedLanguage, + hasImage: result.hasImage, + }; + } else { + return { + file, + error: "Failed to fetch card information in all available languages", + isAsian: true, + }; + } + } + + return null; +} + +// Generate comment body for PR +function generateCommentBody( + cardResults: CardResult[], + changedFiles: string[], + repoFullName: string, + contextSha: string, +): string { + const successfulCards = cardResults.filter((r) => r.card).length; + const errorCards = cardResults.filter((r) => r.error).length; + const cardsWithoutImages = cardResults.filter((r) => r.card && !r.hasImage).length; + + let commentBody = `## 🃏 ${successfulCards + errorCards} Card${successfulCards + errorCards !== 1 ? "s" : ""} Changed\n\n`; + + if (cardResults.length === 0) { + commentBody += + changedFiles.length > 0 + ? "No recognized card files were changed in this PR.\n" + : "No files were changed in this PR.\n"; + return commentBody; + } + + // Add summary if there are errors or cards without images + if (errorCards > 0 || cardsWithoutImages > 0) { + commentBody += `**Details:** ${successfulCards} processed successfully`; + if (cardsWithoutImages > 0) { + commentBody += ` (${cardsWithoutImages} without images)`; + } + if (errorCards > 0) { + commentBody += `, ${errorCards} with errors`; + } + commentBody += `\n\n`; + } + + // Generate detailed card information + for (const item of cardResults) { + const fileUrl = `https://github.com/${repoFullName}/blob/${contextSha}/${item.file}`; + + if (item.card) { + const langInfo = item.usedLanguage ? ` (found using ${item.usedLanguage})` : ""; + const imageStatus = !item.hasImage ? ` (no images)` : ""; + + commentBody += `
${item.card.name} (${item.card.id})${langInfo}${imageStatus}\n\n`; + + if (item.card.image) { + const languages = item.isAsian ? ASIAN_LANGUAGES : INTERNATIONAL_LANGUAGES; + commentBody += renderCardImageTable(item.card, languages); + } else { + commentBody += `

No images available for this card

\n\n`; + } + + commentBody += `**File:** [${item.file}](${fileUrl}) \n`; + commentBody += `**Set:** ${item.card.set?.name || "Unknown"} \n`; + commentBody += `**Rarity:** ${item.card.rarity || "Unknown"}\n\n`; + commentBody += "
\n\n"; + } else if (item.error) { + commentBody += `
⚠️ Error processing ${item.file.split("/").pop()}\n\n`; + commentBody += `**File:** [${item.file}](${fileUrl}) \n`; + commentBody += `**Error:** ${item.error}\n\n`; + commentBody += "
\n\n"; + } + } + + return commentBody; +} + +// Helper to render the card image table +function renderCardImageTable(card: CardData, languages: string[]): string { + let tableMarkdown = `
\n\n`; + tableMarkdown += `| Language | Language | Language |\n`; + tableMarkdown += `|:-------:|:-------:|:-------:|\n`; + + for (let i = 0; i < languages.length; i += 3) { + tableMarkdown += `|`; + + for (let j = 0; j < 3; j++) { + const langIndex = i + j; + if (langIndex < languages.length) { + const lang = languages[langIndex]; + const langName = LANGUAGE_NAMES[lang as keyof typeof LANGUAGE_NAMES] || lang; + const localizedImageUrl = card.image!.replace(/\/[a-z]{2}(-[a-z]{2})?\//, `/${lang}/`); + tableMarkdown += ` ${langName} (${lang})
${card.name} (${langName}) |`; + } else { + tableMarkdown += ` |`; + } + } + tableMarkdown += `\n`; + } + + tableMarkdown += `\n
\n\n`; + return tableMarkdown; +} + +// Post or update PR comment +async function postOrUpdatePRComment( + octokit: ReturnType, + owner: string, + repo: string, + prNumber: number, + commentBody: string, +): Promise { + const commentsResponse = await octokit.rest.issues.listComments({ + owner, + repo, + issue_number: prNumber, + }); + + const botLogin = "github-actions[bot]"; + const existingComment = commentsResponse.data.find( + (comment) => comment.user?.login === botLogin && comment.body?.includes("## 🃏"), + ); + + if (existingComment) { + await octokit.rest.issues.updateComment({ + owner, + repo, + comment_id: existingComment.id, + body: commentBody, + }); + console.log(`Updated existing comment #${existingComment.id} on PR #${prNumber}`); + } else { + await octokit.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body: commentBody, + }); + console.log(`Posted new comment to PR #${prNumber}`); + } +} + +async function run() { + try { + // Initialize GitHub client + const token = core.getInput("github-token", { required: true }); + const octokit = github.getOctokit(token); + const context = github.context; + const { owner, repo } = context.repo; + const repoFullName = `${owner}/${repo}`; + + // Get changed files + const changedFiles = await getChangedFiles(context, octokit); + + // Process card files + const cardResults: CardResult[] = []; + for (const file of changedFiles) { + const result = await processCardFile(file); + if (result) cardResults.push(result); + } + + // Generate comment body + const commentBody = generateCommentBody(cardResults, changedFiles, repoFullName, context.sha); + + // Post or update the comment in the PR + if (context.payload.pull_request) { + const prNumber = context.payload.pull_request.number; + await postOrUpdatePRComment(octokit, owner, repo, prNumber, commentBody); + } + + // Store the generated comment for the workflow + core.setOutput("pr_comment", commentBody); + core.setOutput("files", changedFiles.join(",")); + core.setOutput("cardFiles", JSON.stringify(cardResults)); + } catch (error) { + if (error instanceof Error) { + core.setFailed(error.message); + } else { + core.setFailed("An unknown error occurred"); + } + } +} + +run(); diff --git a/.github/scripts/package.json b/.github/scripts/package.json new file mode 100644 index 000000000..af186bee9 --- /dev/null +++ b/.github/scripts/package.json @@ -0,0 +1,15 @@ +{ + "name": "scripts", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@actions/core": "^1.11.1", + "@actions/github": "^6.0.0", + "@tcgdex/sdk": "^2.6.0" + } +} diff --git a/.github/scripts/tsconfig.json b/.github/scripts/tsconfig.json new file mode 100644 index 000000000..9c62f74b9 --- /dev/null +++ b/.github/scripts/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8421dbc2b..e2abffe69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Build Docker image on: push: branches: - - '*' + - master pull_request: branches: - master @@ -45,7 +45,7 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} + if: ${{ secrets.DOCKERHUB_TOKEN && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} - name: Login to Github Packages uses: docker/login-action@v3 @@ -53,7 +53,7 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.DOCKER_TOKEN }} - if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} + if: ${{ secrets.DOCKER_TOKEN && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -65,7 +65,7 @@ jobs: uses: oven-sh/setup-bun@v2 with: bun-version: latest - + - name: Pre build server run: | bun install --frozen-lockfile @@ -86,6 +86,5 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - push: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} + push: ${{ (secrets.DOCKERHUB_TOKEN || secrets.DOCKER_TOKEN) && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} cache-to: type=gha,mode=max - diff --git a/.github/workflows/comment-pr.yml b/.github/workflows/comment-pr.yml new file mode 100644 index 000000000..57fcdd86b --- /dev/null +++ b/.github/workflows/comment-pr.yml @@ -0,0 +1,36 @@ +name: Card Info Comment + +on: + pull_request: + types: [opened, synchronize] + paths: + - 'data/**/*.ts' + - 'data-asia/**/*.ts' + +jobs: + card-info-comment: + runs-on: ubuntu-latest + continue-on-error: true + defaults: + run: + working-directory: .github/scripts + permissions: + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun i --frozen-lockfile + + - name: Run load-cards script + id: load-cards + run: bun load-cards.js + env: + INPUT_GITHUB-TOKEN: ${{ secrets.GITHUB_TOKEN }}