diff --git a/.github/workflows/ofl-ota-cronjob.yml b/.github/workflows/ofl-ota-cronjob.yml index 3192cc4f05..c7feeca5fb 100644 --- a/.github/workflows/ofl-ota-cronjob.yml +++ b/.github/workflows/ofl-ota-cronjob.yml @@ -12,9 +12,9 @@ name: Daily OFL OTA Update # 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. # -# At the start of each run, the pending-publish table is cleared up to a captured -# timestamp (POST /api/v1/ota/ofl/pending/clear?timestamp=...). Changes merged after -# that timestamp remain pending for the next run. +# Each run captures a timestamp, dispatches the needed OFL publishers, waits for +# all of them to finish, then clears the pending-publish table once. Changes merged +# after that timestamp remain pending for the next run. on: schedule: @@ -34,32 +34,14 @@ jobs: if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }} runs-on: ubuntu-24.04 steps: - - name: Capture start timestamp and clear OFL pending queue + - name: Capture start timestamp id: start shell: bash - env: - OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }} - OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }} 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; } - timestamp="$(date -u +%s)" echo "timestamp=$timestamp" >> "$GITHUB_OUTPUT" - resp_file="$RUNNER_TEMP/ota-pending-clear-response.json" - status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \ - "${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear?timestamp=$timestamp" \ - -H "Authorization: Bearer $OTA_API_KEY")" - body="$(cat "$resp_file")" - echo "$body" - - if [ "$status" != "200" ]; then - echo "::error::OTA pending-clear call failed with HTTP $status" - exit 1 - fi - - name: Checkout repository uses: actions/checkout@v7 with: @@ -93,15 +75,17 @@ jobs: # by $branch would never match anything except main. post_merge_profiles.yml # genuinely runs per-branch (this dispatch below sets --ref "$branch"), # so its history is the real per-branch checkpoint. It also means a - # failed publish naturally gets retried tomorrow: the checkpoint only - # advances on a run that actually succeeded. + # failed publish naturally gets retried tomorrow. Only runs marked by + # this cron count, so an unrelated successful manual run cannot advance + # the OFL checkpoint. # --method GET is required, not cosmetic: gh api defaults to POST # whenever -f fields are present unless a method is given # explicitly, and POST on this list-runs endpoint 404s - confirmed # on real Actions infrastructure, not just reasoned about. - since="$(gh api --method GET "repos/${{ github.repository }}/actions/workflows/post_merge_profiles.yml/runs" \ - -f status=success -f branch="$branch" -f per_page=1 \ - --jq '.workflow_runs[0].run_started_at // empty')" + successful_runs="$(gh api --method GET "repos/${{ github.repository }}/actions/workflows/post_merge_profiles.yml/runs" \ + -f status=success -f branch="$branch" -f per_page=100 --paginate \ + --jq '.workflow_runs[] | select((.display_title // "") | contains("[OFL cron "))')" + since="$(jq -rs 'sort_by(.run_started_at) | last.run_started_at // empty' <<< "$successful_runs")" if [ -z "$since" ]; then echo "No prior successful run for $branch; checking OFL changes up to $SCAN_UNTIL." @@ -124,16 +108,106 @@ jobs: fi if [ "$changed" = true ]; then - # Tolerate a per-branch failure (e.g. a pre-existing release branch - # whose post_merge_profiles.yml predates the vendor/auto_publish - # inputs) rather than aborting the whole scan under set -e. - if ! gh workflow run post_merge_profiles.yml \ + dispatch_id="${GITHUB_RUN_ID}-${branch//\//-}" + # Record successful dispatches for the barrier step below. A + # dispatch failure prevents clearing, so the branch is retried + # on the next cron run. + if gh workflow run post_merge_profiles.yml \ --repo "${{ github.repository }}" \ --ref "$branch" \ - -f vendor="$VENDOR" -f auto_publish=true; then - echo "::warning::failed to dispatch post_merge_profiles.yml for $branch - its post_merge_profiles.yml at this ref may predate the vendor/auto_publish inputs" + -f vendor="$VENDOR" -f auto_publish=true \ + -f ofl_cron_dispatch_id="$dispatch_id"; then + printf '%s\t%s\n' "$branch" "$dispatch_id" >> "$RUNNER_TEMP/ofl-dispatches.tsv" + else + echo "::error::failed to dispatch post_merge_profiles.yml for $branch" + printf '%s\n' "$branch" >> "$RUNNER_TEMP/ofl-dispatch-failures.txt" fi fi echo "::endgroup::" done + + - name: Wait for OFL publishers + id: wait + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DISPATCHES_FILE: ${{ runner.temp }}/ofl-dispatches.tsv + DISPATCH_FAILURES_FILE: ${{ runner.temp }}/ofl-dispatch-failures.txt + run: | + set -euo pipefail + + if [ -s "$DISPATCH_FAILURES_FILE" ]; then + echo "::error::one or more OFL publisher workflows could not be dispatched:" + sed 's/^/ - /' "$DISPATCH_FAILURES_FILE" + exit 1 + fi + + if [ ! -s "$DISPATCHES_FILE" ]; then + echo "No OFL publisher workflows were dispatched; pending queue will not be cleared." + echo "publishers_dispatched=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + : > "$RUNNER_TEMP/ofl-run-ids.tsv" + while IFS=$'\t' read -r branch dispatch_id; do + [ -n "$branch" ] || continue + echo "Waiting for OFL publisher on $branch ($dispatch_id)" + + run_id="" + for _ in {1..120}; do + runs_json="$(gh api --method GET \ + "repos/${{ github.repository }}/actions/workflows/post_merge_profiles.yml/runs" \ + -f branch="$branch" -f event=workflow_dispatch -f per_page=100)" + run_id="$(jq -r --arg marker "[OFL cron $dispatch_id]" \ + '[.workflow_runs[] | select((.display_title // "") | contains($marker))] \ + | sort_by(.created_at) | last | .id // empty' <<< "$runs_json")" + [ -n "$run_id" ] && break + sleep 5 + done + + if [ -z "$run_id" ]; then + echo "::error::could not find dispatched post_merge_profiles run for $branch ($dispatch_id)" + exit 1 + fi + printf '%s\t%s\n' "$branch" "$run_id" >> "$RUNNER_TEMP/ofl-run-ids.tsv" + done < "$DISPATCHES_FILE" + + all_success=true + while IFS=$'\t' read -r branch run_id; do + [ -n "$run_id" ] || continue + echo "Watching OFL publisher run $run_id for $branch" + if ! gh run watch "$run_id" --repo "${{ github.repository }}" --exit-status; then + all_success=false + fi + done < "$RUNNER_TEMP/ofl-run-ids.tsv" + + if [ "$all_success" != true ]; then + echo "::error::one or more OFL publisher workflows failed; pending queue will not be cleared" + exit 1 + fi + echo "publishers_dispatched=true" >> "$GITHUB_OUTPUT" + + - name: Clear OFL pending queue + if: steps.wait.outputs.publishers_dispatched == 'true' + shell: bash + env: + OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }} + OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }} + TIMESTAMP: ${{ steps.start.outputs.timestamp }} + 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; } + + resp_file="$RUNNER_TEMP/ota-pending-clear-response.json" + status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \ + "${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear?timestamp=$TIMESTAMP" \ + -H "Authorization: Bearer $OTA_API_KEY")" + body="$(cat "$resp_file")" + echo "$body" + + if [ "$status" != "200" ]; then + echo "::error::OTA pending-clear call failed with HTTP $status" + exit 1 + fi diff --git a/.github/workflows/post_merge_profiles.yml b/.github/workflows/post_merge_profiles.yml index 90bd9ccf7e..04ffd0429f 100644 --- a/.github/workflows/post_merge_profiles.yml +++ b/.github/workflows/post_merge_profiles.yml @@ -1,5 +1,14 @@ name: Post-merge profiles +run-name: >- + Post-merge profiles${{ + inputs.ofl_cron_dispatch_id != '' && + inputs.vendor == 'OrcaFilamentLibrary' && + (inputs.auto_publish == true || inputs.auto_publish == 'true') && + format(' [OFL cron {0}]', inputs.ofl_cron_dispatch_id) || + '' + }} + # 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 @@ -59,6 +68,12 @@ on: required: false type: boolean default: false + ofl_cron_dispatch_id: + description: >- + Unique marker supplied by the trusted OFL daily cron so it can find + and wait for this dispatched workflow run. + required: false + type: string permissions: contents: read