name: SPT Release Build on: schedule: - cron: "0 4 * * *" # Nightly should trigger at 4am UTC (11pm EST). repository_dispatch: types: [build-trigger] workflow_dispatch: inputs: buildTag: description: "The tag to build on" required: true type: string concurrency: group: spt-release-build cancel-in-progress: true jobs: prepare: runs-on: ubuntu-latest container: image: refringe/spt-build-node:1.1.0 outputs: proceed: ${{ steps.check-existence.outputs.proceed }} is_nightly: ${{ steps.determine-context.outputs.is_nightly }} branch_server: ${{ steps.determine-context.outputs.branch_server }} branch_modules: ${{ steps.determine-context.outputs.branch_modules }} branch_launcher: ${{ steps.determine-context.outputs.branch_launcher }} target_tag: ${{ steps.determine-context.outputs.target_tag }} build_type: ${{ steps.determine-build-type.outputs.build_type }} client_version: ${{ steps.versions.outputs.client_version }} spt_version: ${{ steps.versions.outputs.spt_version }} steps: - name: Determine Build Context id: determine-context shell: bash env: EVENT_NAME: ${{ github.event_name }} CLIENT_PAYLOAD_TAG: ${{ github.event.client_payload.tag }} WORKFLOW_INPUT_TAG: ${{ github.event.inputs.buildTag }} run: | echo "Determining build context..." if [[ "$EVENT_NAME" == "schedule" ]]; then echo "is_nightly=true" >> $GITHUB_OUTPUT echo "branch_server=4.0.0-DEV" >> $GITHUB_OUTPUT echo "branch_modules=4.0.0-DEV" >> $GITHUB_OUTPUT echo "branch_launcher=4.0.0-DEV" >> $GITHUB_OUTPUT echo "Context is nightly build" else echo "is_nightly=false" >> $GITHUB_OUTPUT # Determine the tag based on the event type if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then TAG_NAME="$WORKFLOW_INPUT_TAG" elif [[ "$EVENT_NAME" == "repository_dispatch" ]]; then TAG_NAME="$CLIENT_PAYLOAD_TAG" else echo "Unsupported event: $EVENT_NAME" exit 1 fi if [[ -z "$TAG_NAME" ]]; then echo "No tag provided in event payload." exit 1 fi echo "target_tag=$TAG_NAME" >> $GITHUB_OUTPUT echo "Target tag is $TAG_NAME" fi - name: Determine Build Type id: determine-build-type shell: bash run: | if [[ "${{ steps.determine-context.outputs.is_nightly }}" == "true" ]]; then BUILD_TYPE="bleeding" else TARGET_TAG="${{ steps.determine-context.outputs.target_tag }}" TARGET_TAG_UPPER="${TARGET_TAG^^}" BUILD_TYPE="debug" if [[ "$TARGET_TAG_UPPER" =~ -BEM ]]; then #BUILD_TYPE="bleedingmods" BUILD_TYPE="bleeding" # CHOMP! KEK elif [[ "$TARGET_TAG_UPPER" =~ -BE ]]; then BUILD_TYPE="bleeding" elif [[ "$TARGET_TAG_UPPER" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then BUILD_TYPE="release" fi fi echo "build_type=$BUILD_TYPE" >> $GITHUB_OUTPUT echo "Build type is $BUILD_TYPE" - name: Check Existence id: check-existence shell: bash run: | PROCEED="true" if [[ "${{ steps.determine-context.outputs.is_nightly }}" == "true" ]]; then declare -A BRANCHES=( [Server]="https://github.com/sp-tarkov/server.git@${{ steps.determine-context.outputs.branch_server }}" [Modules]="https://github.com/sp-tarkov/modules.git@${{ steps.determine-context.outputs.branch_modules }}" [Launcher]="https://github.com/sp-tarkov/launcher.git@${{ steps.determine-context.outputs.branch_launcher }}" ) for REPO_NAME in "${!BRANCHES[@]}"; do REPO_URL="${BRANCHES[$REPO_NAME]%@*}" BRANCH="${BRANCHES[$REPO_NAME]##*@}" echo "Checking for branch $BRANCH in $REPO_NAME..." if ! git ls-remote --heads $REPO_URL $BRANCH | grep -q $BRANCH; then echo "Branch $BRANCH not found in $REPO_URL" PROCEED="false" break fi done else TAG="${{ steps.determine-context.outputs.target_tag }}" REPOS=("https://github.com/sp-tarkov/server.git" "https://github.com/sp-tarkov/modules.git" "https://github.com/sp-tarkov/launcher.git") for REPO in "${REPOS[@]}"; do echo "Checking for tag $TAG in $REPO..." if ! git ls-remote --tags $REPO $TAG | grep -q $TAG; then echo "Tag $TAG not found in $REPO" PROCEED="false" break fi done fi echo "proceed=$PROCEED" >> $GITHUB_OUTPUT echo "Matches found. Proceeding with build." - name: Tag Not Found if: steps.check-existence.outputs.proceed == 'false' run: | echo "Required branch/tag not found in one or more repositories, halting workflow." exit 1 - name: Extract Versions id: versions shell: bash run: | rm -rf /workspace/SPT/Build/server-core git init /workspace/SPT/Build/server-core cd /workspace/SPT/Build/server-core git remote add origin https://github.com/sp-tarkov/server.git git config core.sparseCheckout true echo "project/assets/configs/core.json" >> .git/info/sparse-checkout if [[ "${{ steps.determine-context.outputs.is_nightly }}" == "true" ]]; then REF=${{ steps.determine-context.outputs.branch_server }} else REF=${{ steps.determine-context.outputs.target_tag }} fi git fetch --depth=1 origin "${REF}" git checkout FETCH_HEAD cd project/assets/configs SPT_VERSION=$(jq -r '.sptVersion' core.json) FULL_VERSION=$(jq -r '.compatibleTarkovVersion' core.json) CLIENT_VERSION=${FULL_VERSION##*.} echo "client_version=$CLIENT_VERSION" >> $GITHUB_OUTPUT echo "spt_version=$SPT_VERSION" >> $GITHUB_OUTPUT echo "Client version is $CLIENT_VERSION" echo "SPT version is $SPT_VERSION" build-server: needs: prepare if: needs.prepare.outputs.proceed == 'true' runs-on: windows-latest outputs: server_commit: ${{ steps.commit-hash.outputs.server_commit }} steps: - name: Setup Git Target shell: pwsh run: | if ("${{ needs.prepare.outputs.is_nightly }}" -eq "true") { echo "TARGET=${{ needs.prepare.outputs.branch_server }}" >> $env:GITHUB_ENV } else { echo "TARGET=${{ needs.prepare.outputs.target_tag }}" >> $env:GITHUB_ENV } - name: Clone Server shell: pwsh run: | $serverPath = "$env:GITHUB_WORKSPACE\SPT\Build\server" # Delete old remnants of the last build. if (Test-Path $serverPath) { Remove-Item -Recurse -Force $serverPath } # Decide which ref to clone. if ("${{ needs.prepare.outputs.is_nightly }}" -eq "true") { $TARGET = "${{ needs.prepare.outputs.branch_server }}" } else { $TARGET = "${{ needs.prepare.outputs.target_tag }}" } Write-Host "Cloning target: $TARGET" git clone https://github.com/sp-tarkov/server.git --branch $TARGET --depth 1 $serverPath # Pull LFS files. Set-Location $serverPath git lfs install --local git lfs pull - name: Output Commit Hash id: commit-hash shell: pwsh run: echo "server_commit=$(git rev-parse --short HEAD)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append working-directory: ./SPT/Build/server - uses: actions/setup-node@v4 with: node-version-file: "SPT/Build/server/project/.nvmrc" cache: "npm" cache-dependency-path: "SPT/Build/server/project/package.json" - name: Install NPM Dependencies run: npm install working-directory: ./SPT/Build/server/project - name: Build Server shell: pwsh run: npm run build:${{ needs.prepare.outputs.build_type }} -- --arch=x64 --platform=win32 working-directory: ./SPT/Build/server/project - name: Upload Server Artifact uses: actions/upload-artifact@v4 with: name: server-artifact path: ./SPT/Build/server/project/build/ retention-days: 1 if-no-files-found: error build-modules: needs: prepare if: needs.prepare.outputs.proceed == 'true' runs-on: ubuntu-latest container: image: refringe/spt-build-dotnet:1.0.0 steps: - name: Clone Modules id: clone-modules shell: bash run: | rm -rf /workspace/SPT/Build/modules if [[ "${{ needs.prepare.outputs.is_nightly }}" == "true" ]]; then BRANCH=${{ needs.prepare.outputs.branch_modules }} echo "Cloning modules from branch $BRANCH" git clone https://github.com/sp-tarkov/modules.git --branch "$BRANCH" --depth 1 /workspace/SPT/Build/modules else TAG=${{ needs.prepare.outputs.target_tag }} echo "Cloning modules from tag $TAG" git clone https://github.com/sp-tarkov/modules.git --branch "$TAG" --depth 1 /workspace/SPT/Build/modules fi cd /workspace/SPT/Build/modules echo "modules_commit=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - name: Download Client Module Package shell: bash env: MODULE_DOMAIN: ${{ secrets.MODULE_DOMAIN }} run: | DIR_MANAGED="/workspace/SPT/Build/modules/project/Shared/Managed" DOWNLOAD_PATH="$DIR_MANAGED/${{ needs.prepare.outputs.client_version }}.7z" DOWNLOAD_URL="${MODULE_DOMAIN}/${{ needs.prepare.outputs.client_version }}.7z" echo "Downloading Client Module Package from $DOWNLOAD_URL to $DOWNLOAD_PATH" mkdir -p "$DIR_MANAGED" wget -q -O "$DOWNLOAD_PATH" "$DOWNLOAD_URL" || { echo "Failed to download the module package." exit 1 } if [ ! -s "$DOWNLOAD_PATH" ]; then echo "The module package does not exist or is empty." exit 1 fi echo "Download Successful: $DOWNLOAD_PATH" - name: Decompress Client Module Package shell: bash run: | cd /workspace/SPT/Build/modules/project/Shared/Managed 7z x ${{ needs.prepare.outputs.client_version }}.7z -aoa echo "Client module package decompressed." - name: Delete Client Module Package shell: bash run: | cd /workspace/SPT/Build/modules/project/Shared/Managed rm -f ${{ needs.prepare.outputs.client_version }}.7z echo "Client module package deleted." - name: Build Modules shell: bash run: | cd /workspace/SPT/Build/modules/project dotnet build -c Release -p:Version=${{ needs.prepare.outputs.spt_version }} printf "\nBuilt!\n\n" - name: Upload Modules Artifact uses: actions/upload-artifact@v4 with: name: modules-artifact path: /workspace/SPT/Build/modules/project/Build retention-days: 1 if-no-files-found: error build-launcher: needs: prepare if: needs.prepare.outputs.proceed == 'true' runs-on: ubuntu-latest container: image: refringe/spt-build-dotnet:1.0.0 steps: - name: Clone Launcher id: clone-launcher shell: bash run: | rm -rf /workspace/SPT/Build/launcher if [[ "${{ needs.prepare.outputs.is_nightly }}" == "true" ]]; then BRANCH=${{ needs.prepare.outputs.branch_launcher }} echo "Cloning launcher from branch $BRANCH" git clone https://github.com/sp-tarkov/launcher.git --branch "$BRANCH" --depth 1 /workspace/SPT/Build/launcher else TAG=${{ needs.prepare.outputs.target_tag }} echo "Cloning launcher from tag $TAG" git clone https://github.com/sp-tarkov/launcher.git --branch "$TAG" --depth 1 /workspace/SPT/Build/launcher fi cd /workspace/SPT/Build/launcher echo "launcher_commit=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - name: Build Launcher shell: bash run: | cd /workspace/SPT/Build/launcher/project dotnet build printf "\nBuilt!\n\n" - name: Upload Launcher Artifact uses: actions/upload-artifact@v4 with: name: launcher-artifact path: /workspace/SPT/Build/launcher/project/Build retention-days: 1 if-no-files-found: error assemble-and-publish: needs: [prepare, build-server, build-modules, build-launcher] runs-on: ubuntu-latest container: image: refringe/spt-build-node:1.1.0 steps: - name: Clean Directory shell: bash run: | rm -rf release build mkdir -p release - name: Download Server Artifact uses: actions/download-artifact@v4 with: name: server-artifact path: release/ - name: Download Modules Artifact uses: actions/download-artifact@v4 with: name: modules-artifact path: release/ - name: Download Launcher Artifact uses: actions/download-artifact@v4 with: name: launcher-artifact path: release/ - name: Clone Build Project uses: actions/checkout@v4 with: repository: sp-tarkov/build path: build - name: Merge Static Assets and Dynamic Files shell: bash run: cp -rvf build/static-assets/* release/ - name: List Release Contents shell: bash run: tree release - name: Generate Release Filename id: generate-filename shell: bash run: | BUILD_TYPE=${{ needs.prepare.outputs.build_type }} SPT_VERSION=${{ needs.prepare.outputs.spt_version }} CLIENT_VERSION=${{ needs.prepare.outputs.client_version }} SERVER_COMMIT=${{ needs.build-server.outputs.server_commit }} TARGET_TAG=${{ needs.prepare.outputs.target_tag }} DATE=$(date +%Y%m%d) if [[ "${{ needs.prepare.outputs.is_nightly }}" == "true" ]]; then BASE_NAME="SPT-NIGHTLY-${SPT_VERSION}-${CLIENT_VERSION}-${SERVER_COMMIT}-${DATE}" else UPPER_BUILD_TYPE=$(echo "$BUILD_TYPE" | tr '[:lower:]' '[:upper:]') UPPER_TARGET_TAG=$(echo "$TARGET_TAG" | tr '[:lower:]' '[:upper:]') if [ "$BUILD_TYPE" = "release" ]; then BASE_NAME="SPT-${SPT_VERSION}-${CLIENT_VERSION}-${SERVER_COMMIT}" else TAG_PART="" if [[ "$UPPER_TARGET_TAG" == *-*-* ]]; then SUFFIX="${UPPER_TARGET_TAG##*-}" if [ "$SUFFIX" != "$UPPER_TARGET_TAG" ]; then TAG_PART="-${SUFFIX}" fi fi if [ -n "$TAG_PART" ]; then BASE_NAME="SPT-${UPPER_BUILD_TYPE}-${SPT_VERSION}-${CLIENT_VERSION}-${SERVER_COMMIT}${TAG_PART}" else BASE_NAME="SPT-${UPPER_BUILD_TYPE}-${SPT_VERSION}-${CLIENT_VERSION}-${SERVER_COMMIT}-${DATE}" fi fi fi echo "base_name=$BASE_NAME" >> $GITHUB_OUTPUT echo "build_name=${BASE_NAME}.7z" >> $GITHUB_OUTPUT echo "Release filename: ${BASE_NAME}.7z" - name: Compress Release id: compress-release shell: bash run: | cd release 7z a -mx=9 -m0=lzma2 "../${{ steps.generate-filename.outputs.build_name }}" ./* echo "Release compressed as ${{ steps.generate-filename.outputs.build_name }}." FILE_SIZE_MB=$(stat -c %s "../${{ steps.generate-filename.outputs.build_name }}" | awk '{printf "%.2f MB", $1 / 1024 / 1024}') FILE_HASH=$(md5sum "../${{ steps.generate-filename.outputs.build_name }}" | awk '{print $1}' | xxd -r -p | base64) echo "file_size_mb=$FILE_SIZE_MB" >> $GITHUB_OUTPUT echo "file_hash=$FILE_HASH" >> $GITHUB_OUTPUT - name: R2 Upload if: needs.prepare.outputs.build_type == 'release' env: SPT_VERSION: ${{ needs.prepare.outputs.spt_version }} CLIENT_VERSION: ${{ needs.prepare.outputs.client_version }} FILE_HASH: ${{ steps.compress-release.outputs.file_hash }} R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }} R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }} R2_FRONT: ${{ secrets.R2_FRONT }} shell: bash run: | # Configure Rclone echo '[r2] type = s3 provider = Cloudflare access_key_id = '"$R2_ACCESS_KEY"' secret_access_key = '"$R2_SECRET_ACCESS_KEY"' region = auto endpoint = '"$R2_ENDPOINT"' acl = public-read' > ./rclone.conf # Generate Release JSON File echo "{ \"AkiVersion\": \"${SPT_VERSION}\", \"ClientVersion\": \"${CLIENT_VERSION}\", \"Mirrors\": [{ \"DownloadUrl\": \"${R2_FRONT}/${{ steps.generate-filename.outputs.build_name }}\", \"Hash\": \"${FILE_HASH}\" }] }" > ./release.json echo "Current Local Directory:" ls -lah echo "Current Remote Directory:" rclone ls r2:${R2_BUCKET_NAME} --config ./rclone.conf # Remove old .7z files from the bucket rclone lsf r2:${R2_BUCKET_NAME} --config ./rclone.conf --files-only --include="*.7z" --absolute > files-to-remove.txt echo "Files to be deleted:" cat files-to-remove.txt rclone delete r2:${R2_BUCKET_NAME} --config ./rclone.conf --files-from=files-to-remove.txt --max-depth=1 -vv # Upload the .7z file using rclone with the above config rclone copy "./${{ steps.generate-filename.outputs.build_name }}" r2:${R2_BUCKET_NAME} --config ./rclone.conf -vv # Upload the JSON file using rclone with the above config rclone copy "./release.json" r2:${R2_BUCKET_NAME} --config ./rclone.conf -vv echo "R2 Upload completed." - name: Upload Release to HTTPS Source id: upload-https-7z shell: bash run: | sshpass -p "${{ secrets.SFTP_PASSWORD }}" scp -v -o "Port=${{ secrets.SFTP_PORT }}" -o "ConnectTimeout=20" -o "StrictHostKeyChecking=no" "${{ steps.generate-filename.outputs.build_name }}" ${{ secrets.SFTP_USERNAME }}@${{ secrets.SFTP_HOST }}:/public/builds echo "link_https=${{ secrets.SFTP_MIRROR_LINK }}/builds/${{ steps.generate-filename.outputs.build_name }}" >> $GITHUB_OUTPUT echo "HTTPS Upload completed: ${{ secrets.SFTP_MIRROR_LINK }}/builds/${{ steps.generate-filename.outputs.build_name }}" - name: Post Build Info to Discord env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} BUILD_TYPE: ${{ needs.prepare.outputs.build_type }} BASE_NAME: ${{ steps.generate-filename.outputs.base_name }} BUILD_NAME: ${{ steps.generate-filename.outputs.build_name }} FILE_SIZE_MB: ${{ steps.compress-release.outputs.file_size_mb }} FILE_HASH: ${{ steps.compress-release.outputs.file_hash }} LINK_HTTPS: ${{ steps.upload-https-7z.outputs.link_https }} IS_NIGHTLY: ${{ needs.prepare.outputs.is_nightly }} shell: bash run: | UPPER_BUILD_TYPE=$(echo "$BUILD_TYPE" | tr '[:lower:]' '[:upper:]') FOOTER_MESSAGES=("You look great today!" "Powered by coffee" "Life's too short to remove USB safely" "Did you remember to hydrate today?" "Have you tried turning it off and on again?" "There's no place like 127.0.0.1" "In Chomp we trust" "Beep boop, I'm a bot" "Keep calm and commit your code" "This isn't a bug, it's an undocumented feature." "May the source be with you" "Go to bed, Terk" "Please direct all support requests to Drakia" "Meaw" "Chomp approves of this message" "Chomp is life, Chomp is love" "Drakia denies all involvement" "Drakia left this note here just to confuse you" "Cabbit is the reason we can’t have nice things" "Cabbit voted against this message" "Powered by caffeine, chaos, and Chomp" "RaiRai says hi-hi" "RaiRai wants to remind you that sarcasm is a skill" "Refringe just wobbled" "Refringe is watching" "Refringe rewrote this embed thirty times" "Refringe, professional button-pusher extraordinaire" "Sarix would like you to reconsider your choices" "Stella is currently judging your grammar" "Stella just gave this embed a 6/10" "Waffle has entered the chat, and now it’s weird" "Waffle is too busy stacking layers of chaos" "Waffle would like to speak to the manager of logic") FOOTER_MESSAGE="${FOOTER_MESSAGES[$RANDOM % ${#FOOTER_MESSAGES[@]}]}" TIMESTAMP=$(date --iso-8601=seconds) MODS="" if [[ "${{ needs.prepare.outputs.is_nightly }}" == "true" ]]; then EMBED_COLOR=16705372 EMBED_DESCRIPTION='A new nightly build is available. These are untested and considered unstable. Absolutely no support is provided. **If you ask for help you will be banned from the #dev-builds channel without explanation.** 7-Zip is *required* to extract the release. The download link is temporary.' MODS='disabled' else if [ "$BUILD_TYPE" == "bleeding" ]; then EMBED_COLOR=15548997 EMBED_DESCRIPTION='A new bleeding edge build is available. These are strictly for testing issues *and not for general gameplay*. 7-Zip is *required* to extract the release. The download link is temporary.' MODS='disabled' elif [ "$BUILD_TYPE" == "bleedingmods" ]; then EMBED_COLOR=15548997 EMBED_DESCRIPTION='A new bleeding edge build is available. These are strictly for testing issues *and not for general gameplay*. 7-Zip is *required* to extract the release. The download link is temporary.' MODS='enabled' elif [ "$BUILD_TYPE" == "debug" ]; then EMBED_COLOR=2123412 EMBED_DESCRIPTION=$'A new debug build is available. These have extra-verbose logging enabled *for testing*. 7-Zip is *required* to extract the release. The download link is temporary.' MODS='enabled' else EMBED_COLOR=5763719 EMBED_DESCRIPTION=$'A new stable build is now ready for download. 7-Zip is *required* to extract the release. Have fun! 🎉' MODS='enabled' fi fi fields_json='[ {"name": "Name", "value": "'"$BASE_NAME"'"}, {"name": "Build Type", "value": "'"$BUILD_TYPE"'", "inline": true}, {"name": "Mods", "value": "'"$MODS"'", "inline": true}, {"name": "File Size", "value": "'"$FILE_SIZE_MB"'", "inline": true}, {"name": "File Hash", "value": "'"$FILE_HASH"'"}, {"name": "Download", "value": "'"$LINK_HTTPS"'"} ]' payload=$(jq -n \ --argjson fields "$fields_json" \ --arg EMBED_DESCRIPTION "$EMBED_DESCRIPTION" \ --argjson EMBED_COLOR "$EMBED_COLOR" \ --arg FOOTER_MESSAGE "$FOOTER_MESSAGE" \ --arg TIMESTAMP "$TIMESTAMP" \ '{ "content": $EMBED_DESCRIPTION, "embeds": [ { "title": "Build Information", "color": $EMBED_COLOR, "fields": $fields, "footer": {"text": $FOOTER_MESSAGE, "icon_url": "https://i.imgur.com/28JJJec.png"}, "timestamp": $TIMESTAMP } ], "username": "BuildBot", "avatar_url": "https://i.imgur.com/28JJJec.png" }') echo "$payload" > payload_discord.json echo "Payload Generated:" cat payload_discord.json echo "Sending Payload..." curl -H "Content-Type: application/json" \ -X POST \ --data-binary @payload_discord.json \ $DISCORD_WEBHOOK_URL