Compare commits

...

3 Commits

Author SHA1 Message Date
Ian Chua
ffb2946b20 feat: compare with environment variable FOLDER_MERGERS for verified folders 2026-09-10 20:09:37 +08:00
Ian Chua
e89a05b1e4 fix: repo name 2026-09-10 16:38:03 +08:00
Ian Chua
02a8011b97 feat: add CI to generate OPC for OTA workflow 2026-09-10 16:27:23 +08:00
3 changed files with 292 additions and 2 deletions

View File

@@ -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
# <vendor>.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

View File

@@ -0,0 +1,253 @@
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 (<vendor>.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$
# <orca_ver>_<vendor>_<profile_version>_<UTC yyyymmddHHMM>.zip (zip root: <vendor>.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' }}
# FOLDER_MERGERS is an environment-scoped variable, shared with the PR
# merge bot. Keep this environment free of protection rules so this
# push-triggered job does not wait for a reviewer.
environment: merge-delegation
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
env:
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
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
# A vendor is eligible only when both the profile directory and its
# sibling bundle JSON are covered by at least one FOLDER_MERGERS
# grant. The account part is intentionally ignored here: this is a
# post-merge safety check, not an authorization check for a command.
grants=()
while IFS= read -r raw_line; do
line="${raw_line#"${raw_line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[ -n "$line" ] || continue
[[ "$line" == \#* ]] && continue
[[ "$line" == *:* ]] || continue
grant="${line#*:}"
grant="${grant#"${grant%%[![:space:]]*}"}"
grant="${grant%"${grant##*[![:space:]]}"}"
while [[ "$grant" == */ ]]; do grant="${grant%/}"; done
grants+=("$grant")
done <<< "${FOLDER_MERGERS:-}"
is_granted() {
local path="$1"
local grant
for grant in "${grants[@]:-}"; do
if [[ "$path" == "$grant" || "$path" == "$grant/"* ]]; then
return 0
fi
done
return 1
}
unauthorized=()
for v in "${vendors[@]}"; do
if ! is_granted "resources/profiles/$v" || ! is_granted "resources/profiles/$v.json"; then
unauthorized+=("$v")
fi
done
if [ "${#unauthorized[@]}" -ne 0 ]; then
echo "Changed vendor profiles are not covered by FOLDER_MERGERS: ${unauthorized[*]}"
echo "No profile caches will be published for this push."
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:-<none>}' 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"

View File

@@ -23,7 +23,8 @@ int main(int argc, char* argv[])
#else
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
#endif
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)");
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)")
("vendor,v", po::value<std::string>()->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<std::string>();
const int log_level = vm["log_level"].as<int>();
const std::string vendor = vm["vendor"].as<std::string>();
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 <vendor>.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"))