From 0030bed519d3f0f646f4d1acee6e7c63bb0cdb0e Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 23 Sep 2026 20:37:25 +0800 Subject: [PATCH] fix: ota updates workflow --- .github/workflows/ofl-ota-cronjob.yml | 88 ++++++++++++++++++++++ .github/workflows/post_merge_profiles.yml | 91 +++++++++++++++++++++-- 2 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ofl-ota-cronjob.yml diff --git a/.github/workflows/ofl-ota-cronjob.yml b/.github/workflows/ofl-ota-cronjob.yml new file mode 100644 index 0000000000..05466e585c --- /dev/null +++ b/.github/workflows/ofl-ota-cronjob.yml @@ -0,0 +1,88 @@ +name: Daily OFL OTA Update + +# This workflow is intended for creating and publishing the OrcaFilamentLibrary (OFL) OPC package to +# https://github.com/OrcaSlicer/orcaslicer-profiles, which generates an OTA update. +# This cronjob runs daily at 00:00 UTC every day and scans main plus every release/vX.Y.Z branch for +# changes to resources/profiles/OrcaFilamentLibrary since that branch's own last successful run. Any +# branch with no changes is skipped; each changed branch gets its own post_merge_profiles.yml dispatch. +# +# OFL has no dedicated FOLDER_MERGERS grant (it isn't merged through the PR merge-bot delegation +# scheme), so post_merge_profiles.yml is dispatched with an explicit `vendor` input, which that +# workflow trusts and uses to bypass the FOLDER_MERGERS check for this trigger. That same explicit- +# vendor-dispatch path is also what makes post_merge_profiles.yml call the OTA auto-publish API after +# uploading - see post_merge_profiles.yml for both sides of that contract. + +on: + schedule: + - cron: "0 0 * * *" + + workflow_dispatch: + +permissions: + actions: write # list this workflow's past runs and dispatch post_merge_profiles.yml + contents: read + +env: + VENDOR: OrcaFilamentLibrary + +jobs: + daily-job: + if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }} + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + # Full history: the per-branch "since last successful run" check below + # needs to look arbitrarily far back if a prior run failed or was skipped. + fetch-depth: 0 + + - name: Fetch all branches + shell: bash + run: git fetch origin '+refs/heads/*:refs/remotes/origin/*' + + - name: Scan branches and publish changed OFL profiles + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + mapfile -t branches < <( + gh api "repos/${{ github.repository }}/branches" --paginate --jq '.[].name' \ + | grep -E '^(main|release/v[0-9]+\.[0-9]+\.[0-9]+)$' | sort -u + ) + + for branch in "${branches[@]}"; do + echo "::group::$branch" + + since="$(gh api "repos/${{ github.repository }}/actions/workflows/ofl-ota-cronjob.yml/runs" \ + -f status=success -f branch="$branch" -f per_page=1 \ + --jq '.workflow_runs[0].run_started_at // empty')" + + if [ -z "$since" ]; then + echo "No prior successful run for $branch; treating OFL as changed." + changed=true + else + changed_files="$(git log --since="$since" --name-only --pretty=format: "origin/$branch" -- \ + resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \ + | sed '/^$/d')" + if [ -n "$changed_files" ]; then + echo "OFL changed on $branch since $since:" + echo "$changed_files" + changed=true + else + echo "No OFL changes on $branch since $since." + changed=false + fi + fi + + if [ "$changed" = true ]; then + gh workflow run post_merge_profiles.yml \ + --repo "${{ github.repository }}" \ + --ref "$branch" \ + -f vendor="$VENDOR" + fi + + echo "::endgroup::" + done diff --git a/.github/workflows/post_merge_profiles.yml b/.github/workflows/post_merge_profiles.yml index 67780e37bd..a70b255719 100644 --- a/.github/workflows/post_merge_profiles.yml +++ b/.github/workflows/post_merge_profiles.yml @@ -8,6 +8,12 @@ name: Post-merge profiles # only then does it become a live OTA update - this workflow does none of that # last part (no changelog, no R2, no webhook). # +# A workflow_dispatch carrying a `vendor` input (e.g. the daily OFL cron - OFL has +# no FOLDER_MERGERS grant, since it isn't merged through the PR merge-bot delegation +# scheme) publishes that vendor directly and skips the FOLDER_MERGERS check below. +# workflow_dispatch is already a trusted, explicit trigger, unlike the automatic +# push-diff path the FOLDER_MERGERS check exists to gate. +# # Asset contract expected by OrcaCloud's release scanner: # ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$ # ___.zip (zip root: .opc) @@ -17,13 +23,25 @@ name: Post-merge profiles on: push: branches: + # once v2.5.0 stable is released, this will be removed, so nightly won't receive OTA updates. - main - - release/* + # release/vX.Y.Z point-release branches only, not the release/vX.Y working + # branch profile PRs land on first - "v*.*.*" requires two literal dots, + # which release/vX.Y (one dot) doesn't have. + - release/v*.*.* paths: - 'resources/profiles/**' - '.github/workflows/post_merge_profiles.yml' workflow_dispatch: + inputs: + vendor: + description: >- + Publish only this vendor, bypassing the FOLDER_MERGERS grant check. + For trusted explicit dispatches only (e.g. the OFL nightly cron). + Leave empty to fall back to diffing the triggering commit. + required: false + type: string permissions: contents: read @@ -66,8 +84,23 @@ jobs: shell: bash env: FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }} + DISPATCH_VENDOR: ${{ github.event_name == 'workflow_dispatch' && inputs.vendor || '' }} run: | set -euo pipefail + + # Explicit vendor dispatch (e.g. the OFL cron): trust the caller and + # skip both the git-diff detection and the FOLDER_MERGERS check below. + if [ -n "$DISPATCH_VENDOR" ]; then + v="$DISPATCH_VENDOR" + json="resources/profiles/$v.json" + if [ ! -f "$json" ] || { [ ! -d "resources/profiles/$v" ] && ! jq -e '.version' "$json" >/dev/null 2>&1; }; then + echo "::error::vendor '$v' has no resources/profiles/$v.json with a profile directory or version field" + exit 1 + fi + echo "vendors=$v" >> "$GITHUB_OUTPUT" + exit 0 + fi + base='${{ github.event.before }}' head='${{ github.sha }}' # Zero SHA (branch created / force push) or manual dispatch: fall back @@ -101,6 +134,9 @@ jobs: # 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. + # An ineligible vendor (e.g. OrcaFilamentLibrary, which has no grant) + # is dropped on its own - it never blocks other vendors in the same + # push from publishing. grants=() while IFS= read -r raw_line; do line="${raw_line#"${raw_line%%[![:space:]]*}"}" @@ -127,19 +163,21 @@ jobs: return 1 } + authorized=() unauthorized=() for v in "${vendors[@]}"; do - if ! is_granted "resources/profiles/$v" || ! is_granted "resources/profiles/$v.json"; then + if is_granted "resources/profiles/$v" && is_granted "resources/profiles/$v.json"; then + authorized+=("$v") + else unauthorized+=("$v") fi done if [ "${#unauthorized[@]}" -ne 0 ]; then - echo "vendors=" >> "$GITHUB_OUTPUT" - exit 0 + echo "::warning::skipping vendor(s) with no FOLDER_MERGERS grant (no asset built or published for them this run): ${unauthorized[*]}" fi - echo "vendors=${vendors[*]}" >> "$GITHUB_OUTPUT" + echo "vendors=${authorized[*]}" >> "$GITHUB_OUTPUT" - name: Resolve Orca version id: orca @@ -242,3 +280,46 @@ jobs: echo "### Published to \`$repo\` release \`$RELEASE_TAG\`" for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done } >> "$GITHUB_STEP_SUMMARY" + + - name: Notify OTA auto-publish + # Only the explicit-vendor-dispatch path (e.g. the OFL cron) goes live + # automatically. The ordinary push path stays human-gated: assets land on + # the profiles release above, and a maintainer still has to attach a + # changelog and hit Publish in OrcaCloud's OTA Manager for those. + if: steps.vendors.outputs.vendors != '' && github.event_name == 'workflow_dispatch' && inputs.vendor != '' + shell: bash + env: + OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }} + OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }} + ASSET_DIR: ${{ steps.pkg.outputs.dir }} + run: | + set -euo pipefail + [ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; } + [ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; } + + mapfile -t zip_files < <(cd "$ASSET_DIR" && ls -1 *.zip) + filenames_json="$(printf '%s\n' "${zip_files[@]}" | jq -R . | jq -s .)" + payload="$(jq -n --argjson filenames "$filenames_json" '{filenames: $filenames}')" + + resp_file="$RUNNER_TEMP/ota-auto-publish-response.json" + status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \ + "${OTA_API_BASE_URL%/}/api/v1/ota/auto-publish" \ + -H "Authorization: Bearer $OTA_API_KEY" \ + -H 'Content-Type: application/json' \ + -d "$payload")" + body="$(cat "$resp_file")" + echo "$body" + + if [ "$status" != "200" ]; then + echo "::error::OTA auto-publish call failed with HTTP $status" + exit 1 + fi + + # A 200 can still carry per-file "error" results (e.g. NOT_FOUND); the + # asset is already safely published to the profiles release above, but + # it never went live, so treat that as a failure worth surfacing loudly. + error_count="$(jq '[.results[] | select(.status == "error")] | length' <<< "$body")" + if [ "$error_count" != "0" ]; then + jq -r '.results[] | select(.status == "error") | "::error::\(.filename): \(.code) - \(.message)"' <<< "$body" + exit 1 + fi