mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
6e5b011059
Each run now stages what it built as DorkNet-<release>-PC-Full.zip, -PC-Patch.zip, -Quest.xapk and -Quest-Installer.zip, writes their sizes and SHA-256 to a manifest, and uploads them with a static page to an S3 bucket served read-only (ci/downloads). The build scripts keep their own names, which the launcher depends on; only the published copies are renamed. PUBLISH_DOWNLOADS (on by default) turns the upload off per run. Jenkins archives the same renamed files instead of the raw build output.
241 lines
11 KiB
Groovy
241 lines
11 KiB
Groovy
// DorkNet client builds — PC (patcher zip + full client) and Quest (.xapk).
|
|
//
|
|
// Runs on the "dorknet-builder" agent from ci/jenkins (an inbound agent with
|
|
// Python + LIEF, .NET, the Android NDK/build-tools, apktool, restic and rclone).
|
|
//
|
|
// Stock builds come out of the restic build archive on the FTP. Where it lives
|
|
// is server configuration, not part of the repo: the FTP host, login and the
|
|
// restic password are Jenkins credentials, and the repository folders come
|
|
// from the agent's environment (ARCHIVE_PC_REPO / ARCHIVE_QUEST_REPO), each
|
|
// overridable per run. A build is picked by snapshot tags.
|
|
//
|
|
// On a push, only the platforms whose code changed are rebuilt. A manual run
|
|
// picks the platforms with PLATFORMS. See ci/jenkins/README.md.
|
|
//
|
|
// Finished builds are renamed for players and published to the downloads page
|
|
// (ci/downloads) unless PUBLISH_DOWNLOADS is off. The build scripts keep their
|
|
// own file names, which the launcher depends on.
|
|
|
|
// The builds this branch targets. Any other archived build can be picked per
|
|
// run with the *_SNAPSHOT_TAGS parameters.
|
|
def PC_VERSION_KEY = 'march_2023_03_21'
|
|
def PC_DEFAULT_TAGS = 'manifest=7490748483298966814' // Steam 2023-03-21 (appVersion 20230317)
|
|
def QUEST_DEFAULT_TAGS = 'version-code=2128210' // Quest 20230317
|
|
|
|
// How this branch's builds are listed on the downloads page.
|
|
def RELEASE_ID = '2023.03'
|
|
def RELEASE_TITLE = 'March 2023'
|
|
def RELEASE_GAME_VERSION = '2023-03-21'
|
|
|
|
def buildPc = false
|
|
def buildQuest = false
|
|
|
|
pipeline {
|
|
agent { label 'dorknet-builder' }
|
|
|
|
options {
|
|
timestamps()
|
|
ansiColor('xterm')
|
|
disableConcurrentBuilds()
|
|
// Full clients and xapks are several GB each: keep artifacts for the
|
|
// last few runs only.
|
|
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '3'))
|
|
timeout(time: 3, unit: 'HOURS')
|
|
}
|
|
|
|
parameters {
|
|
choice(name: 'PLATFORMS', choices: ['both', 'pc', 'quest'],
|
|
description: 'What to build on a manual run. Push builds rebuild whatever changed.')
|
|
booleanParam(name: 'PC_FULL_CLIENT', defaultValue: true,
|
|
description: 'Also build the full PC client (stock build + MelonLoader + mod + config). The patcher zip is always built.')
|
|
string(name: 'PC_SNAPSHOT_TAGS', defaultValue: '',
|
|
description: "restic tags picking the stock PC build, comma = AND. Empty: ${PC_DEFAULT_TAGS}")
|
|
string(name: 'QUEST_SNAPSHOT_TAGS', defaultValue: '',
|
|
description: "restic tags picking the stock Quest build, comma = AND. Empty: ${QUEST_DEFAULT_TAGS}")
|
|
string(name: 'PC_REPO', defaultValue: '',
|
|
description: 'PC restic repository folder on the FTP. Empty: ARCHIVE_PC_REPO set on the agent.')
|
|
string(name: 'QUEST_REPO', defaultValue: '',
|
|
description: 'Quest restic repository folder on the FTP. Empty: ARCHIVE_QUEST_REPO set on the agent.')
|
|
booleanParam(name: 'LIST_SNAPSHOTS', defaultValue: false,
|
|
description: 'Only list the snapshots matching the tags in both repositories (to find a build), then stop.')
|
|
booleanParam(name: 'PUBLISH_DOWNLOADS', defaultValue: true,
|
|
description: 'Publish what this run built to the downloads page. Push builds publish too.')
|
|
}
|
|
|
|
stages {
|
|
stage('Plan') {
|
|
steps {
|
|
script {
|
|
def pushed = !currentBuild.getBuildCauses('com.cloudbees.jenkins.GitHubPushCause').isEmpty() ||
|
|
!currentBuild.getBuildCauses('hudson.triggers.SCMTrigger$SCMTriggerCause').isEmpty()
|
|
if (params.LIST_SNAPSHOTS) {
|
|
buildPc = false
|
|
buildQuest = false
|
|
} else if (pushed) {
|
|
def changed = []
|
|
for (cs in currentBuild.changeSets) {
|
|
for (entry in cs.items) changed.addAll(entry.affectedPaths)
|
|
}
|
|
def touches = { prefixes -> changed.any { p -> prefixes.any { p.startsWith(it) } } }
|
|
def common = ['Jenkinsfile', 'ci/jenkins/']
|
|
buildPc = touches(common + ['DorkNet.ClientMod/', 'tools/build_pc.py'])
|
|
buildQuest = touches(common + ['DorkNet.Quest/'])
|
|
echo "Push build; ${changed.size()} changed file(s)"
|
|
} else {
|
|
buildPc = params.PLATFORMS in ['both', 'pc']
|
|
buildQuest = params.PLATFORMS in ['both', 'quest']
|
|
}
|
|
echo "Build PC: ${buildPc}, Quest: ${buildQuest}"
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('List snapshots') {
|
|
when { expression { params.LIST_SNAPSHOTS } }
|
|
steps {
|
|
withArchive {
|
|
withEnv(["PC_REPO_OVERRIDE=${params.PC_REPO}", "QUEST_REPO_OVERRIDE=${params.QUEST_REPO}",
|
|
"PC_TAGS=${params.PC_SNAPSHOT_TAGS}", "QUEST_TAGS=${params.QUEST_SNAPSHOT_TAGS}"]) {
|
|
sh '''
|
|
export RESTIC_CACHE_DIR="$DORKNET_CACHE/restic"
|
|
pc="${PC_REPO_OVERRIDE:-${ARCHIVE_PC_REPO:?set ARCHIVE_PC_REPO on the agent or PC_REPO on the run}}"
|
|
quest="${QUEST_REPO_OVERRIDE:-${ARCHIVE_QUEST_REPO:?set ARCHIVE_QUEST_REPO on the agent or QUEST_REPO on the run}}"
|
|
echo "== PC: $pc"; archive-restore list "$pc" "$PC_TAGS"
|
|
echo "== Quest: $quest"; archive-restore list "$quest" "$QUEST_TAGS"
|
|
'''
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('PC patcher zip') {
|
|
when { expression { buildPc } }
|
|
steps {
|
|
sh "python3 tools/build_pc.py patcher --version-key ${PC_VERSION_KEY} --output-dir out/pc"
|
|
}
|
|
}
|
|
|
|
stage('PC full client') {
|
|
when { expression { buildPc && params.PC_FULL_CLIENT } }
|
|
steps {
|
|
script {
|
|
def base = withArchive {
|
|
withEnv(["REPO_OVERRIDE=${params.PC_REPO}", "TAGS=${params.PC_SNAPSHOT_TAGS ?: PC_DEFAULT_TAGS}"]) {
|
|
sh(returnStdout: true, script: '''
|
|
export RESTIC_CACHE_DIR="$DORKNET_CACHE/restic"
|
|
repo="${REPO_OVERRIDE:-${ARCHIVE_PC_REPO:?set ARCHIVE_PC_REPO on the agent or PC_REPO on the run}}"
|
|
archive-restore restore "$repo" "$TAGS" "$DORKNET_CACHE/pc" --exclude /.DepotDownloader
|
|
''').trim().readLines().last()
|
|
}
|
|
}
|
|
withCredentials([file(credentialsId: 'clientmod-config', variable: 'CLIENTMOD_CONFIG')]) {
|
|
withEnv(["BASE_CLIENT=${base}"]) {
|
|
sh """
|
|
python3 tools/build_pc.py client \\
|
|
--base-client "\$BASE_CLIENT" \\
|
|
--patcher-zip out/pc/dorknet-clientpatch-${PC_VERSION_KEY}.zip \\
|
|
--config "\$CLIENTMOD_CONFIG" \\
|
|
--output-zip out/pc/dorknet-client-${PC_VERSION_KEY}.zip
|
|
"""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('Quest') {
|
|
when { expression { buildQuest } }
|
|
steps {
|
|
script {
|
|
def stock = withArchive {
|
|
withEnv(["REPO_OVERRIDE=${params.QUEST_REPO}", "TAGS=${params.QUEST_SNAPSHOT_TAGS ?: QUEST_DEFAULT_TAGS}"]) {
|
|
sh(returnStdout: true, script: '''
|
|
export RESTIC_CACHE_DIR="$DORKNET_CACHE/restic"
|
|
repo="${REPO_OVERRIDE:-${ARCHIVE_QUEST_REPO:?set ARCHIVE_QUEST_REPO on the agent or QUEST_REPO on the run}}"
|
|
archive-restore restore "$repo" "$TAGS" "$DORKNET_CACHE/quest"
|
|
''').trim().readLines().last()
|
|
}
|
|
}
|
|
withCredentials([
|
|
file(credentialsId: 'quest-keystore', variable: 'QUEST_KEYSTORE'),
|
|
string(credentialsId: 'quest-keystore-password', variable: 'QUEST_KEYSTORE_PASSWORD'),
|
|
file(credentialsId: 'quest-config', variable: 'QUEST_CONFIG'),
|
|
]) {
|
|
withEnv(["QUEST_STOCK=${stock}"]) {
|
|
// The hook table for the build's versionCode must already be
|
|
// resolved and committed under DorkNet.Quest/targets/; build.py
|
|
// checks it against the APK's libil2cpp.so.
|
|
sh '''
|
|
set -eu
|
|
obb="$(ls "$QUEST_STOCK"/obbs/main.*.obb | head -n1)"
|
|
code="$(basename "$obb" | cut -d. -f2)"
|
|
table="$(ls DorkNet.Quest/targets/*-"$code".json 2>/dev/null | head -n1 || true)"
|
|
if [ -z "$table" ]; then
|
|
echo "No resolved hook table for versionCode $code in DorkNet.Quest/targets/." >&2
|
|
echo "Resolve one locally with build.py build --dump-cs and commit it." >&2
|
|
exit 1
|
|
fi
|
|
mkdir -p out/quest
|
|
python3 DorkNet.Quest/scripts/build.py build \
|
|
--apk "$QUEST_STOCK/app.apk" --obb "$obb" \
|
|
--targets-json "$table" \
|
|
--config "$QUEST_CONFIG" \
|
|
--keystore "$QUEST_KEYSTORE" --keystore-pass "$QUEST_KEYSTORE_PASSWORD" \
|
|
--work-dir "$WORKSPACE/quest-work" \
|
|
--out-apk "$WORKSPACE/out/quest/dorknet-recroom-$code.apk"
|
|
'''
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('Downloads') {
|
|
when { expression { buildPc || buildQuest } }
|
|
steps {
|
|
sh """
|
|
python3 ci/downloads/publish.py stage --out out --release ${RELEASE_ID} \\
|
|
--title '${RELEASE_TITLE}' --game-version ${RELEASE_GAME_VERSION} --version-key ${PC_VERSION_KEY}
|
|
"""
|
|
script {
|
|
if (params.PUBLISH_DOWNLOADS) {
|
|
// The bucket's S3 endpoint and name come from the agent's
|
|
// environment (DOWNLOADS_S3_ENDPOINT / DOWNLOADS_BUCKET).
|
|
withCredentials([usernamePassword(credentialsId: 'downloads-s3',
|
|
usernameVariable: 'DOWNLOADS_S3_KEY_ID', passwordVariable: 'DOWNLOADS_S3_SECRET')]) {
|
|
sh 'python3 ci/downloads/publish.py upload --staged out/downloads'
|
|
}
|
|
} else {
|
|
echo 'PUBLISH_DOWNLOADS is off: the files are only archived.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
post {
|
|
success {
|
|
// The renamed, player-facing files (and release.json): the same set the
|
|
// downloads page offers.
|
|
archiveArtifacts artifacts: 'out/downloads/**', allowEmptyArchive: true, fingerprint: true
|
|
}
|
|
cleanup {
|
|
// Runs after archiving. Keeps the restic cache and restored stock
|
|
// builds ($DORKNET_CACHE on the agent, outside the workspace); drops
|
|
// the workspace's build output and scratch.
|
|
cleanWs(deleteDirs: true, notFailBuild: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// FTP + restic credentials for archive-restore.
|
|
def withArchive(Closure body) {
|
|
withCredentials([
|
|
usernamePassword(credentialsId: 'archive-ftp', usernameVariable: 'ARCHIVE_FTP_USER', passwordVariable: 'ARCHIVE_FTP_PASSWORD'),
|
|
string(credentialsId: 'archive-ftp-host', variable: 'ARCHIVE_FTP_HOST'),
|
|
string(credentialsId: 'archive-restic-password', variable: 'RESTIC_PASSWORD'),
|
|
]) {
|
|
return body()
|
|
}
|
|
}
|