diff --git a/tools/ci/annotate_dm.sh b/tools/ci/annotate_dm.sh new file mode 100644 index 0000000000000..e43f930ba1acc --- /dev/null +++ b/tools/ci/annotate_dm.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +set -euo pipefail +tools/bootstrap/python -m dm_annotator "$@" diff --git a/tools/ci/build.ps1 b/tools/ci/build.ps1 new file mode 100644 index 0000000000000..4694cc3be58d4 --- /dev/null +++ b/tools/ci/build.ps1 @@ -0,0 +1,10 @@ +if(!(Test-Path -Path "C:/byond")){ + bash tools/ci/download_byond.sh + [System.IO.Compression.ZipFile]::ExtractToDirectory("C:/byond.zip", "C:/") + Remove-Item C:/byond.zip +} + +bash tools/ci/install_node.sh +bash tools/build/build -Werror + +exit $LASTEXITCODE diff --git a/tools/ci/build_spaceman_dmm.sh b/tools/ci/build_spaceman_dmm.sh new file mode 100644 index 0000000000000..d63aeac2cc850 --- /dev/null +++ b/tools/ci/build_spaceman_dmm.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -euo pipefail + +source dependencies.sh + +cd $HOME/SpacemanDMM + +if [ ! -d .git ] +then + git init + git remote add origin https://github.com/SpaceManiac/SpacemanDMM.git +fi + +git fetch origin --depth=1 $SPACEMAN_DMM_COMMIT_HASH +git reset --hard FETCH_HEAD + +cargo build --release --bin $1 +cp target/release/$1 ~ +~/$1 --version diff --git a/tools/ci/check_filedirs copy.sh b/tools/ci/check_filedirs copy.sh new file mode 100644 index 0000000000000..59f05d5f5be9d --- /dev/null +++ b/tools/ci/check_filedirs copy.sh @@ -0,0 +1,15 @@ +#!/bin/bash +if [ -n "$1" ] +then + dme=$1 +else + echo "ERROR: Specify a DME to check" + exit 1 +fi + +if [[ $(awk '/BEGIN_FILE_DIR/{flag=1;next}/END_FILE_DIR/{flag=0}flag' $dme | wc -l) -ne 1 ]] +then + echo "ERROR: File DIR was ticked, please untick it, see: https://tgstation13.org/phpBB/viewtopic.php?f=5&t=321 for more" + exit 1 +fi + diff --git a/tools/ci/check_misc.sh b/tools/ci/check_misc.sh new file mode 100644 index 0000000000000..5cb1c860c3361 --- /dev/null +++ b/tools/ci/check_misc.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail + +find . -name "*.php" -print0 | xargs -0 -n1 php -l +find . -name "*.json" -not -path "*/node_modules/*" -print0 | xargs -0 python3 ./tools/json_verifier.py diff --git a/tools/ci/ci_config.txt b/tools/ci/ci_config.txt new file mode 100644 index 0000000000000..51e08e6328ba1 --- /dev/null +++ b/tools/ci/ci_config.txt @@ -0,0 +1,10 @@ +SQL_ENABLED +ADDRESS 127.0.0.1 +PORT 3306 +FEEDBACK_DATABASE tg_ci +FEEDBACK_TABLEPREFIX +FEEDBACK_LOGIN root +FEEDBACK_PASSWORD +LAVALAND_BUDGET 0 +SPACE_BUDGET 0 +AUXTOOLS_ENABLED diff --git a/tools/ci/ci_dependencies.sh b/tools/ci/ci_dependencies.sh new file mode 100644 index 0000000000000..fd1bee5ea88a9 --- /dev/null +++ b/tools/ci/ci_dependencies.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +#Project dependencies file +#Contains versions of programs that we might need to install for CI purposes - do not add anything here that is REQUIRED to run the project, this is just for CI. + +export RIPGREP_VERSION=14.0.3 diff --git a/tools/ci/download_byond.sh b/tools/ci/download_byond.sh new file mode 100644 index 0000000000000..19b7f2017078f --- /dev/null +++ b/tools/ci/download_byond.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e +source dependencies.sh +echo "Downloading BYOND version $BYOND_MAJOR.$BYOND_MINOR" +curl "http://www.byond.com/download/build/$BYOND_MAJOR/$BYOND_MAJOR.${BYOND_MINOR}_byond.zip" -o C:/byond.zip diff --git a/tools/ci/install_auxlua.sh b/tools/ci/install_auxlua.sh new file mode 100644 index 0000000000000..c24ccb497d17f --- /dev/null +++ b/tools/ci/install_auxlua.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +source dependencies.sh + +mkdir -p ~/.byond/bin +wget -nv -O ~/.byond/bin/libauxlua.so "https://github.com/$AUXLUA_REPO/releases/download/$AUXLUA_VERSION/libauxlua.so" +chmod +x ~/.byond/bin/libauxlua.so +ldd ~/.byond/bin/libauxlua.so diff --git a/tools/ci/install_ripgrep.sh b/tools/ci/install_ripgrep.sh new file mode 100644 index 0000000000000..455027d93a6eb --- /dev/null +++ b/tools/ci/install_ripgrep.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +source tools/ci/ci_dependencies.sh + +cargo install ripgrep --features pcre2 --version $RIPGREP_VERSION diff --git a/tools/ci/run_server.sh b/tools/ci/run_server.sh new file mode 100644 index 0000000000000..baf172d8ec3d8 --- /dev/null +++ b/tools/ci/run_server.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +MAP=$1 + +echo Testing $MAP + +tools/deploy.sh ci_test +mkdir ci_test/config +mkdir ci_test/data + +#test config +cp tools/ci/ci_config.txt ci_test/config/config.txt + +#set the map +cp _maps/$MAP.json ci_test/data/next_map.json + +cd ci_test +DreamDaemon tgstation.dmb -close -trusted -verbose -params "log-directory=ci" + +cd .. + +mkdir -p data/screenshots_new +cp -r ci_test/data/screenshots_new data/screenshots_new + +cat ci_test/data/logs/ci/clean_run.lk diff --git a/tools/ci/show_screenshot_test_results.js b/tools/ci/show_screenshot_test_results.js new file mode 100644 index 0000000000000..607ad9854d676 --- /dev/null +++ b/tools/ci/show_screenshot_test_results.js @@ -0,0 +1,236 @@ +import fetch, { FormData, fileFrom } from "node-fetch"; +import fs from "fs"; +import path from "path"; +import process from "process"; + +const createComment = (screenshotFailures, zipFileUrl) => { + const formatScreenshotFailure = ({ directory, diffUrl, newUrl, oldUrl }) => { + const img = (url) => { + if (url) { + return `![](${url})`; + } else { + return "None produced."; + } + }; + + return `| ${directory} | ${img(oldUrl)} | ${img(newUrl)} | ${img(diffUrl)} |`; + }; + + return ` + Screenshot tests failed! + + ${zipFileUrl ? `[Download zip file of new screenshots.](${zipFileUrl})` : "No zip file could be produced, this is a bug!"} + + ## Diffs +
+ See snapshot diffs + + | Name | Expected image | Produced image | Diff | + | :--: | :------------: | :------------: | :--: | + ${screenshotFailures.map(formatScreenshotFailure).join("\n")} +
+ + ## Help +
+ What is this? + + Screenshot tests make sure that specific icons look the same as they did before. + This is important for elements that often mistakenly change, such as alien species. + + If the produced image looks broken, then it is possible your code caused a bug. + Make sure to test in game to see if you can fix it. +
+ +
+ I am changing sprites, it's supposed to look different. + + If the newly produced sprites are correct, then the tests should be updated. + + You can either: + + 1. Right-click the "produced image", and save it in \`code/modules/unit_tests/screenshots/NAME.png\`. + 2. Download and extract [this zip file](${zipFileUrl}) in the root of your repository, and commit. + + If you need help, you can ask maintainers either on Discord or on this pull request. +
+ +
+ This is a false positive. + + If you are sure your code did not cause this failure, especially if it's inconsistent, + then you may have found a false positive. + + Ask maintainers to rerun the test. + + If you need help, you can ask maintainers either on Discord or on this pull request. +
+ `.replace(/\t/g, ''); // If we keep tabs, it'll become a code block. +}; + +export async function showScreenshotTestResults({ github, context, exec }) { + const { FILE_HOUSE_KEY } = process.env; + + // Check if bad-screenshots is in the artifacts + const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + const badScreenshots = artifacts.find(({ name }) => name === 'bad-screenshots'); + if (!badScreenshots) { + console.log("No bad screenshots found"); + return; + } + + // Download the screenshots from the artifacts + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: badScreenshots.id, + archive_format: "zip", + }); + + fs.writeFileSync("bad-screenshots.zip", Buffer.from(download.data)); + + await exec.exec("unzip bad-screenshots.zip -d bad-screenshots"); + + const prNumberFile = path.join("bad-screenshots", "pull_request_number.txt"); + + if (!fs.existsSync(prNumberFile)) { + console.log("No PR number found"); + return; + } + + const prNumber = parseInt(fs.readFileSync(prNumberFile, "utf8"), 10); + if (!prNumber) { + console.log("No PR number found"); + return; + } + + fs.rmSync(prNumberFile); + + // Validate the PR + const result = await github.graphql(`query($owner:String!, $repo:String!, $prNumber:Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $prNumber) { + commits(last: 1) { + nodes { + commit { + checkSuites(first: 10) { + nodes { + id + } + } + } + } + } + } + } + }`, { + owner: context.repo.owner, + repo: context.repo.repo, + prNumber, + }); + + const validPr = result + .repository + .pullRequest + .commits + .nodes[0] + .commit + .checkSuites + .nodes + .some(({ id }) => id === context.payload.workflow_run.check_suite_node_id); + + if (!validPr) { + console.log(`PR #${prNumber} is not valid (expected check suite ID ${context.payload.workflow_run.check_suite_node_id})`); + return; + } + + // Upload the screenshots + // 1. Loop over the bad-screenshots directory + // 2. Upload the screenshot + // 3. Save the URL + const uploadFile = async (filename) => { + if (!fs.existsSync(filename)) { + return; + } + + const formData = new FormData(); + + formData.set("key", FILE_HOUSE_KEY); + + formData.set("file", await fileFrom(filename), path.basename(filename)); + + return fetch("https://file.house/api/upload", { + method: "POST", + body: formData, + }) + .then(response => response.json()) + .then(response => { + console.log(response); + return response; + }) + .then(({ url }) => url); + }; + + const screenshotFailures = []; + + for (const directory of fs.readdirSync("bad-screenshots")) { + console.log(`Uploading screenshots for ${directory}`); + + let diffUrl; + let newUrl; + let oldUrl; + + await Promise.all([ + uploadFile(path.join("bad-screenshots", directory, "new.png")).then(url => newUrl = url), + uploadFile(path.join("bad-screenshots", directory, "old.png")).then(url => oldUrl = url), + uploadFile(path.join("bad-screenshots", directory, "diff.png")).then(url => diffUrl = url), + ]); + + console.log(`New URL (${directory}): ${newUrl}`); + console.log(`Old URL (${directory}): ${oldUrl}`); + console.log(`Diff URL (${directory}): ${diffUrl}`); + + screenshotFailures.push({ directory, diffUrl, newUrl, oldUrl }); + } + + if (screenshotFailures.length === 0) { + console.log("No screenshot failures found"); + return; + } + + // Upload zip file for quick fixes + const zipFilePath = path.join("data", "screenshot-update"); + const finalDestination = path.join( + zipFilePath, + "code", "modules", "unit_tests", "screenshots", + ) + + fs.mkdirSync(finalDestination, { recursive: true }); + + for (const { directory } of screenshotFailures) { + fs.copyFileSync( + path.join("bad-screenshots", directory, "new.png"), + path.join(finalDestination, `${directory}.png`), + ) + } + + await exec.exec("zip", ["-r", `../screenshot-update.zip`, "."], { + cwd: zipFilePath, + }); + + const zipUrl = await uploadFile(`${zipFilePath}.zip`); + + // Post the comment + const comment = createComment(screenshotFailures, zipUrl); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: comment, + }); +}