diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 8e1e28db13..753546ba91 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -630,6 +630,18 @@ jobs: name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} path: './build/src/Release/OrcaSlicer_profile_validator' + # generate_system_cache is what scripts/build_preset_cache.sh bakes the + # .opc caches with; it was already built by the "Build system + # preset cache (Linux)" step above. The .opc format is 64-bit + # little-endian native, i.e. identical across every platform Orca ships, + # so only the Linux binary is published. + - name: Upload generate_system_cache Ubuntu + if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} + uses: actions/upload-artifact@v7 + with: + name: generate_system_cache_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} + path: './build/src/dev-utils/Release/generate_system_cache' + - name: Deploy Ubuntu release if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }} uses: WebFreak001/deploy-nightly@v3.2.0 @@ -660,6 +672,17 @@ jobs: 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 + 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 diff --git a/.github/workflows/post_merge_profiles.yml b/.github/workflows/post_merge_profiles.yml new file mode 100644 index 0000000000..e6861efa28 --- /dev/null +++ b/.github/workflows/post_merge_profiles.yml @@ -0,0 +1,202 @@ +name: Post-merge profiles + +# Push-triggered counterpart to check_profiles.yml (which only gates PRs). When a +# profile change lands on main or a release branch, rebuild the affected vendors' +# binary preset caches (.opc) and publish each as a versioned ZIP asset on +# a per-Orca-version release of the profiles repo. From there OrcaCloud's OTA +# Manager lists the asset, a maintainer attaches a changelog and hits Publish, and +# only then does it become a live OTA update - this workflow does none of that +# last part (no changelog, no R2, no webhook). +# +# Asset contract expected by OrcaCloud's release scanner: +# ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$ +# ___.zip (zip root: .opc) +# +# Setup (App + secrets): docs/ota/post-merge-profiles-setup.md + +on: + push: + branches: + - main + - release/* + paths: + - 'resources/profiles/**' + - '.github/workflows/post_merge_profiles.yml' + + workflow_dispatch: + +permissions: + contents: read + +# One run per branch; let a run finish rather than cancel it, since it publishes. +concurrency: + group: post-merge-profiles-${{ github.ref }} + cancel-in-progress: false + +env: + # generate_system_cache is published to this repo's own nightly-builds release + # by build_orca.yml's Linux leg. The job guard pins github.repository to + # OrcaSlicer/OrcaSlicer, so this resolves there. + TOOL_REPO: ${{ github.repository }} + TOOL_ASSET: generate_system_cache_Linux_Ubuntu2404_nightly + # Where per-vendor ZIP assets are published; OrcaCloud's OTA reads this repo. + PROFILES_OWNER: OrcaSlicer + PROFILES_REPO: orcaslicer_profiles + +jobs: + publish_profile_caches: + name: Publish profile caches + if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }} + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + # Enough history to reach github.event.before for the changed-vendor + # diff on a normal push; deeper pushes fall back to HEAD^..HEAD in the + # step below. fetch-depth: 0 would clone all of OrcaSlicer's history. + fetch-depth: 50 + + - name: Resolve changed vendors + id: vendors + shell: bash + run: | + set -euo pipefail + base='${{ github.event.before }}' + head='${{ github.sha }}' + # Zero SHA (branch created / force push) or manual dispatch: fall back + # to this commit's own diff. + if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then + base="$head^" + fi + echo "Diffing $base..$head" + + mapfile -t candidates < <( + git diff --name-only "$base" "$head" -- resources/profiles \ + | sed -nE 's#^resources/profiles/([^/]+)/.*#\1#p; s#^resources/profiles/([^/]+)\.json$#\1#p' \ + | sort -u + ) + + vendors=() + for v in "${candidates[@]:-}"; do + [ -n "$v" ] || continue + json="resources/profiles/$v.json" + # A vendor has a manifest plus either a preset directory or a version + # field; this drops non-vendor files such as blacklist.json. + if [ -f "$json" ] && { [ -d "resources/profiles/$v" ] || jq -e '.version' "$json" >/dev/null 2>&1; }; then + vendors+=("$v") + fi + done + + if [ "${#vendors[@]}" -eq 0 ]; then + echo "No changed vendor profiles in this push; nothing to publish." + echo "vendors=" >> "$GITHUB_OUTPUT" + exit 0 + fi + printf 'Changed vendors: %s\n' "${vendors[*]}" + echo "vendors=${vendors[*]}" >> "$GITHUB_OUTPUT" + + - name: Resolve Orca version + id: orca + if: steps.vendors.outputs.vendors != '' + shell: bash + run: | + set -euo pipefail + raw="$(sed -nE 's/^set\(SoftFever_VERSION "([^"]+)".*/\1/p' version.inc | head -1)" + [ -n "$raw" ] || { echo "::error::could not read SoftFever_VERSION from version.inc"; exit 1; } + orca_ver="${raw%%-*}" + if ! [[ "$orca_ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Orca version '$orca_ver' (from '$raw') is not X.Y.Z"; exit 1 + fi + # release_tag is what the desktop client sends as orca_version and what + # OrcaCloud keys R2 on; orca_ver (X.Y.Z) is the asset-name prefix. + echo "release_tag=$raw" >> "$GITHUB_OUTPUT" + echo "orca_ver=$orca_ver" >> "$GITHUB_OUTPUT" + echo "Orca version: release_tag=$raw asset_prefix=$orca_ver" + + - name: Validate profile versions + id: pver + if: steps.vendors.outputs.vendors != '' + shell: bash + run: | + set -euo pipefail + : > "$RUNNER_TEMP/pver.tsv" + for v in ${{ steps.vendors.outputs.vendors }}; do + pv="$(jq -r '.version // empty' "resources/profiles/$v.json")" + if ! [[ "$pv" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::vendor $v version '${pv:-}' must be 4 numeric parts (A.B.C.D) for the OTA asset name; fix resources/profiles/$v.json" + exit 1 + fi + printf '%s\t%s\n' "$v" "$pv" >> "$RUNNER_TEMP/pver.tsv" + echo "$v -> $pv" + done + + - name: Download generate_system_cache + if: steps.vendors.outputs.vendors != '' + shell: bash + env: + # gh (with the default token) rather than an unauthenticated curl: keeps + # working if TOOL_REPO is ever private and avoids anonymous rate limits. + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + gh release download nightly-builds --repo "$TOOL_REPO" \ + --pattern "$TOOL_ASSET" --output generate_system_cache --clobber + chmod +x generate_system_cache + + - name: Build caches and package assets + id: pkg + if: steps.vendors.outputs.vendors != '' + shell: bash + run: | + set -euo pipefail + # One timestamp for the whole run so a multi-vendor merge groups together. + ts="$(date -u +%Y%m%d%H%M)" + orca_ver='${{ steps.orca.outputs.orca_ver }}' + out="$RUNNER_TEMP/assets" + mkdir -p "$out" + + for v in ${{ steps.vendors.outputs.vendors }}; do + ./generate_system_cache -p "$GITHUB_WORKSPACE/resources/profiles" -v "$v" -l 2 + opc="resources/profiles/$v.opc" + [ -f "$opc" ] || { echo "::error::$opc was not generated"; exit 1; } + pv="$(awk -F'\t' -v v="$v" '$1==v{print $2}' "$RUNNER_TEMP/pver.tsv")" + name="${orca_ver}_${v}_${pv}_${ts}.zip" + ( cd resources/profiles && zip -q -j "$out/$name" "$v.opc" ) + echo "packaged $name" + done + echo "dir=$out" >> "$GITHUB_OUTPUT" + + - name: Mint profiles-repo token + id: token + if: steps.vendors.outputs.vendors != '' + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.PROFILES_APP_ID }} + private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }} + owner: ${{ env.PROFILES_OWNER }} + repositories: ${{ env.PROFILES_REPO }} + + - name: Publish assets to profiles release + if: steps.vendors.outputs.vendors != '' + shell: bash + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + RELEASE_TAG: ${{ steps.orca.outputs.release_tag }} + ASSET_DIR: ${{ steps.pkg.outputs.dir }} + run: | + set -euo pipefail + repo="$PROFILES_OWNER/$PROFILES_REPO" + if ! gh release view "$RELEASE_TAG" --repo "$repo" >/dev/null 2>&1; then + echo "Creating release $RELEASE_TAG on $repo" + gh release create "$RELEASE_TAG" --repo "$repo" \ + --title "$RELEASE_TAG" --notes "Profile cache assets for Orca $RELEASE_TAG." \ + --latest=false + fi + # Asset names are timestamp-unique; a clash means a bug, so don't --clobber. + gh release upload "$RELEASE_TAG" --repo "$repo" "$ASSET_DIR"/*.zip + + { + echo "### Published to \`$repo\` release \`$RELEASE_TAG\`" + for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done + } >> "$GITHUB_STEP_SUMMARY" \ No newline at end of file diff --git a/src/dev-utils/generate_system_cache.cpp b/src/dev-utils/generate_system_cache.cpp index 426ccee997..017db79836 100644 --- a/src/dev-utils/generate_system_cache.cpp +++ b/src/dev-utils/generate_system_cache.cpp @@ -23,7 +23,8 @@ int main(int argc, char* argv[]) #else ("path,p", po::value()->default_value("../../../resources/profiles"), "Path to profiles directory") #endif - ("log_level,l", po::value()->default_value(2), "Log level (0=trace, 2=info, 4=error)"); + ("log_level,l", po::value()->default_value(2), "Log level (0=trace, 2=info, 4=error)") + ("vendor,v", po::value()->default_value(""), "Vendor name. Optional; generate the cache for this vendor only (the Orca filament library is always included as the inheritance base). All vendors if not specified."); // clang-format on po::variables_map vm; @@ -38,6 +39,7 @@ int main(int argc, char* argv[]) const std::string profiles_path = vm["path"].as(); const int log_level = vm["log_level"].as(); + const std::string vendor = vm["vendor"].as(); if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) { std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n"; @@ -59,8 +61,12 @@ int main(int argc, char* argv[]) preset_bundle->set_is_validation_mode(true); preset_bundle->set_default_suppressed(true); preset_bundle->set_generate_vendor_caches(true); + // Empty == every vendor. Otherwise only this vendor (plus the always-loaded + // Orca filament library) is parsed, so only its .opc is written. + preset_bundle->set_vendor_to_validate(vendor); - std::cout << "Loading system presets from: " << profiles_path << "\n"; + std::cout << "Loading system presets from: " << profiles_path + << (vendor.empty() ? "" : " (vendor: " + vendor + ")") << "\n"; try { // In validation mode data_dir() is the profiles directory set above, so the @@ -71,6 +77,14 @@ int main(int argc, char* argv[]) return 1; } + // A specific vendor must have produced its own cache; the always-loaded + // filament library alone would otherwise mask a misspelt or removed name. + if (!vendor.empty() && !fs::exists(fs::path(profiles_path) / (vendor + ".opc"))) { + std::cerr << "No cache was generated for vendor '" << vendor << "' under " << profiles_path + << " - check the vendor name.\n"; + return 1; + } + size_t cache_count = 0; for (auto& entry : fs::directory_iterator(profiles_path)) if (boost::iends_with(entry.path().string(), ".opc"))