From ab42ddeef2549e169dcd1ca978153bcf4f501cac Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Tue, 13 Oct 2026 06:19:07 -0500 Subject: [PATCH] ci: replace deploy-nightly action with a local composite action (#16419) WebFreak001/deploy-nightly still targets Node.js 20, so every main build logged a deprecation warning per deploy step. The action has had no release since 2024 and its Node 24 PR is unmerged. The local action in .github/actions/deploy-nightly uploads the new file under a temporary name and renames it once the upload finishes, so a failed upload keeps the existing nightly asset. It also fails on a missing file before deleting anything, retries transient API errors, times out stalled transfers, handles concurrent runs, and finds the release by its nightly-builds tag. The macOS validator DMG deploy now runs only when that DMG was built. --- .github/actions/deploy-nightly/action.yml | 29 ++++++ .github/actions/deploy-nightly/deploy.sh | 104 ++++++++++++++++++++++ .github/workflows/build_all.yml | 6 +- .github/workflows/build_orca.yml | 60 +++---------- 4 files changed, 148 insertions(+), 51 deletions(-) create mode 100644 .github/actions/deploy-nightly/action.yml create mode 100644 .github/actions/deploy-nightly/deploy.sh diff --git a/.github/actions/deploy-nightly/action.yml b/.github/actions/deploy-nightly/action.yml new file mode 100644 index 0000000000..cfcd1c7fcb --- /dev/null +++ b/.github/actions/deploy-nightly/action.yml @@ -0,0 +1,29 @@ +name: 'Deploy Nightly' +description: 'Upload a file to a release, replacing the asset of the same name' +inputs: + asset_path: + description: 'Path of the file to upload' + required: true + asset_name: + description: 'Asset name on the release' + required: true + content_type: + description: 'Content type of the uploaded file' + required: false + default: 'application/octet-stream' + tag: + description: 'Tag of the release to upload to' + required: false + default: 'nightly-builds' +runs: + using: 'composite' + steps: + - name: Replace release asset + shell: bash + env: + GH_TOKEN: ${{ github.token }} + ASSET_PATH: ${{ inputs.asset_path }} + ASSET_NAME: ${{ inputs.asset_name }} + CONTENT_TYPE: ${{ inputs.content_type }} + RELEASE_TAG: ${{ inputs.tag }} + run: bash "$GITHUB_ACTION_PATH/deploy.sh" diff --git a/.github/actions/deploy-nightly/deploy.sh b/.github/actions/deploy-nightly/deploy.sh new file mode 100644 index 0000000000..9d5b22b9a8 --- /dev/null +++ b/.github/actions/deploy-nightly/deploy.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Replaces one asset of a GitHub release, uploading under a temporary name +# first so a failed upload leaves the old asset in place. +# +# Environment: GH_TOKEN, ASSET_PATH, ASSET_NAME, CONTENT_TYPE, RELEASE_TAG and +# the GITHUB_* variables Actions sets. + +# The $names inside the single-quoted jq programs are jq variables. +# shellcheck disable=SC2016 +set -euo pipefail + +repo_api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY" + +if [ ! -f "$ASSET_PATH" ]; then + echo "::error::$ASSET_PATH does not exist" + exit 1 +fi + +# The speed limit turns a stalled transfer into an error that --retry handles. +curl_opts=(-sS --retry 3 --connect-timeout 30 --speed-limit 1024 --speed-time 60 + -H "Authorization: Bearer $GH_TOKEN") + +# Prints the response body, or reports GitHub's error body and fails. +api() { + local out + if ! out=$(curl "${curl_opts[@]}" --fail-with-body "$@"); then + # A workflow command ends at the first newline. + echo "::error::GitHub API request failed: ${out//[$'\r\n']/ }" >&2 + return 1 + fi + printf '%s\n' "$out" +} + +http_status() { + curl "${curl_opts[@]}" -o /dev/null -w '%{http_code}' "$@" +} + +# jq prints CRLF on Windows. +jqr() { + jq -r "$@" | tr -d '\r' +} + +delete_asset() { + local code + code=$(http_status -X DELETE "$repo_api/releases/assets/$1") + case $code in + # A 404 means a concurrent run already deleted the asset. + 204 | 404) ;; + *) + echo "::error::Deleting release asset $1 failed with HTTP $code" >&2 + return 1 + ;; + esac +} + +list_assets() { + local page=1 json + while :; do + # Callers run this inside $(...), where bash ignores set -e. + json=$(api "$repo_api/releases/$release_id/assets?per_page=100&page=$page") || exit 1 + printf '%s\n' "$json" + [ "$(jqr length <<<"$json")" -eq 100 ] || break + page=$((page + 1)) + done | jq -s add +} + +release=$(api "$repo_api/releases/tags/$RELEASE_TAG") +release_id=$(jqr .id <<<"$release") +upload_url=$(jqr .upload_url <<<"$release") +upload_url=${upload_url%%\{*} + +# Unique per run attempt, so concurrent runs never upload to the same name. +tmp_name="$ASSET_NAME.partial-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + +# Deletes leftover partials older than an hour, an age no upload in progress reaches. +stale_ids=$(list_assets | jqr --arg prefix "$ASSET_NAME.partial-" \ + '.[] | select((.name | startswith($prefix)) and (.created_at | fromdateiso8601) < now - 3600) | .id') +for id in $stale_ids; do + delete_asset "$id" +done + +name_query=$(jqr -n --arg name "$tmp_name" '$name | @uri') +# Uploads once, since a retry after an upload that succeeded fails on the taken name. +new_id=$(api --retry 0 -X POST -H "Content-Type: $CONTENT_TYPE" -T "$ASSET_PATH" \ + "$upload_url?name=$name_query" | jqr .id) +trap 'delete_asset "$new_id" || true' EXIT + +rename_body=$(jq -n --arg name "$ASSET_NAME" '{name: $name}') +# Retries on HTTP 422, which a concurrent run causes by renaming its own upload +# between this delete and this rename. +for _ in 1 2 3; do + old_ids=$(list_assets | jqr --arg name "$ASSET_NAME" '.[] | select(.name == $name) | .id') + for id in $old_ids; do + delete_asset "$id" + done + code=$(http_status -X PATCH -d "$rename_body" "$repo_api/releases/assets/$new_id") + if [ "$code" = 200 ]; then + trap - EXIT + exit 0 + fi + [ "$code" = 422 ] || break +done +echo "::error::Renaming $tmp_name to $ASSET_NAME failed with HTTP $code" >&2 +exit 1 diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 22790fb1e8..8fa7901623 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -441,14 +441,10 @@ jobs: path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak' - name: Deploy Flatpak to nightly release if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' - uses: WebFreak001/deploy-nightly@v3.2.0 + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak - asset_content_type: application/octet-stream - max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted # The asset is /app (the exes link it at runtime) plus the build tree # slimmed to what ctest needs. - name: Package flatpak test asset diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index e49e157358..4d85542c71 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -397,25 +397,18 @@ jobs: - name: Deploy Mac release if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED - uses: WebFreak001/deploy-nightly@v3.2.0 + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg asset_name: OrcaSlicer_Mac_universal_nightly.dmg - asset_content_type: application/octet-stream - max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted + # The DMG is only built when the validator app was. - name: Deploy Mac OrcaSlicer_profile_validator DMG release - if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED - uses: WebFreak001/deploy-nightly@v3.2.0 + if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED && hashFiles(format('OrcaSlicer_profile_validator_Mac_universal_{0}.dmg', env.ver)) != '' + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: ${{ github.workspace }}/OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg asset_name: OrcaSlicer_profile_validator_Mac_universal_nightly.dmg - asset_content_type: application/octet-stream - max_releases: 1 # Windows - name: Set Windows build variables @@ -542,36 +535,27 @@ jobs: - name: Deploy Windows release portable if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED - uses: WebFreak001/deploy-nightly@v3.2.0 + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly_portable.zip - asset_content_type: application/x-zip-compressed - max_releases: 1 + content_type: application/x-zip-compressed - name: Deploy Windows release installer if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED - uses: WebFreak001/deploy-nightly@v3.2.0 + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly.exe - asset_content_type: application/x-msdownload - max_releases: 1 + content_type: application/x-msdownload - name: Deploy Windows OrcaSlicer_profile_validator release if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED - uses: WebFreak001/deploy-nightly@v3.2.0 + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe asset_name: OrcaSlicer_profile_validator_Windows_nightly.exe - asset_content_type: application/x-msdownload - max_releases: 1 + content_type: application/x-msdownload - name: Build MSIX Store package Win if: runner.os == 'Windows' && !vars.SELF_HOSTED @@ -700,14 +684,10 @@ jobs: - name: Deploy Ubuntu release if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }} - uses: WebFreak001/deploy-nightly@v3.2.0 + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly.AppImage - asset_content_type: application/octet-stream - max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted - name: Deploy Ubuntu release if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} run: | @@ -716,36 +696,24 @@ jobs: - name: Deploy Ubuntu OrcaSlicer_profile_validator release if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} - uses: WebFreak001/deploy-nightly@v3.2.0 + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: ./build/src/Release/OrcaSlicer_profile_validator asset_name: OrcaSlicer_profile_validator_Linux${{ env.ubuntu-ver-str }}_nightly - asset_content_type: application/octet-stream - max_releases: 1 - name: Deploy Ubuntu generate_system_cache release if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} - uses: WebFreak001/deploy-nightly@v3.2.0 + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: ./build/src/dev-utils/Release/generate_system_cache asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly - asset_content_type: application/octet-stream - max_releases: 1 - name: Deploy orca_custom_preset_tests if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} - uses: WebFreak001/deploy-nightly@v3.2.0 + uses: ./.github/actions/deploy-nightly with: - upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label} - release_id: 137995723 asset_path: ${{ github.workspace }}/resources/profiles/orca_custom_preset_tests.zip asset_name: orca_custom_preset_tests.zip - asset_content_type: application/octet-stream - max_releases: 1 # The build has just touched everything it can use, so an object # untouched for a week is dead, usually orphaned by a flag change.