From 6d438131b19088a5aaaec3198f270d13f9cffee4 Mon Sep 17 00:00:00 2001 From: metal-face Date: Sat, 23 May 2026 04:26:07 -0400 Subject: [PATCH 1/2] ci: add weekly workflow to sync fork with upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .github/workflows/sync-upstream.yaml. Every Monday at 06:00 UTC (and on-demand via workflow_dispatch) the job fetches juhaku/utoipa, merges upstream/master into a sync/upstream branch with --no-ff so fork-specific commits are preserved, then opens or updates a PR against master. Merge conflicts abort cleanly and open/update a tracking issue with recovery steps instead of pushing broken state. Safeguards: skips push if a human has pushed conflict-resolution commits to an open sync PR; gated to Devolutions/utoipa so forks-of-the-fork don't activate it; falls back to GITHUB_TOKEN if SYNC_PAT is not configured (note: GITHUB_TOKEN-opened PRs do not trigger build.yaml — set SYNC_PAT to get CI on sync PRs). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sync-upstream.yaml | 165 +++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 .github/workflows/sync-upstream.yaml diff --git a/.github/workflows/sync-upstream.yaml b/.github/workflows/sync-upstream.yaml new file mode 100644 index 00000000..3f1c2437 --- /dev/null +++ b/.github/workflows/sync-upstream.yaml @@ -0,0 +1,165 @@ +name: Sync with upstream + +# Weekly sync of this fork with juhaku/utoipa. Produces a PR against master +# that merges upstream while preserving fork-specific commits. +# +# Token note: +# PRs opened by GITHUB_TOKEN do NOT trigger other workflows (build.yaml, +# etc.) — a security guard against recursive CI. To get CI on the sync PR, +# create a fine-grained PAT with `contents: write` + `pull-requests: write` +# on this repo and store it as the `SYNC_PAT` secret. The workflow falls +# back to GITHUB_TOKEN if SYNC_PAT is not set. + +on: + schedule: + # Mondays at 06:00 UTC. + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write + +concurrency: + group: sync-upstream + cancel-in-progress: false + +jobs: + sync: + # Don't activate on forks-of-this-fork. + if: github.repository == 'Devolutions/utoipa' + runs-on: ubuntu-latest + env: + UPSTREAM_REPO: juhaku/utoipa + UPSTREAM_BRANCH: master + FORK_BRANCH: master + SYNC_BRANCH: sync/upstream + steps: + - name: Checkout fork (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.SYNC_PAT || secrets.GITHUB_TOKEN }} + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Add upstream remote and fetch + run: | + git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" + git fetch upstream "${UPSTREAM_BRANCH}" + git fetch origin "${SYNC_BRANCH}" || true + + - name: Determine whether sync is needed + id: check + run: | + BEHIND=$(git rev-list --count "${FORK_BRANCH}..upstream/${UPSTREAM_BRANCH}") + AHEAD=$(git rev-list --count "upstream/${UPSTREAM_BRANCH}..${FORK_BRANCH}") + UPSTREAM_SHA=$(git rev-parse "upstream/${UPSTREAM_BRANCH}") + echo "behind=${BEHIND}" >> "$GITHUB_OUTPUT" + echo "upstream_sha=${UPSTREAM_SHA}" >> "$GITHUB_OUTPUT" + echo "Fork is ${AHEAD} ahead and ${BEHIND} behind upstream (${UPSTREAM_SHA})." + + - name: Attempt merge on sync branch + if: steps.check.outputs.behind != '0' + id: merge + env: + UPSTREAM_SHA: ${{ steps.check.outputs.upstream_sha }} + run: | + git checkout -B "${SYNC_BRANCH}" "${FORK_BRANCH}" + if git merge --no-ff --no-edit \ + -m "Merge upstream ${UPSTREAM_REPO}@${UPSTREAM_BRANCH} (${UPSTREAM_SHA:0:7})" \ + "upstream/${UPSTREAM_BRANCH}"; then + echo "status=clean" >> "$GITHUB_OUTPUT" + else + git merge --abort || true + echo "status=conflict" >> "$GITHUB_OUTPUT" + fi + + - name: Push sync branch + if: steps.merge.outputs.status == 'clean' + id: push + env: + GH_TOKEN: ${{ secrets.SYNC_PAT || secrets.GITHUB_TOKEN }} + run: | + # If an open PR exists and someone has pushed commits to its branch + # (e.g. manual conflict resolution from a previous run), don't + # clobber that work. Comment on the PR instead. + PR_NUM=$(gh pr list --head "${SYNC_BRANCH}" --base "${FORK_BRANCH}" \ + --state open --json number --jq '.[0].number') + if [ -n "${PR_NUM}" ] && git rev-parse --verify "origin/${SYNC_BRANCH}" >/dev/null 2>&1; then + HUMAN_AHEAD=$(git rev-list --count "HEAD..origin/${SYNC_BRANCH}") + if [ "${HUMAN_AHEAD}" -gt 0 ]; then + echo "Open PR #${PR_NUM} has ${HUMAN_AHEAD} commit(s) we don't have — skipping push." + gh pr comment "${PR_NUM}" --body "New upstream commits are available since this PR was opened. Pull \`master\` into this branch and merge \`upstream/master\` again to incorporate them." + echo "skipped=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + fi + git push --force-with-lease origin "${SYNC_BRANCH}" + echo "skipped=false" >> "$GITHUB_OUTPUT" + + - name: Open or update sync PR + if: steps.merge.outputs.status == 'clean' && steps.push.outputs.skipped != 'true' + env: + GH_TOKEN: ${{ secrets.SYNC_PAT || secrets.GITHUB_TOKEN }} + UPSTREAM_SHA: ${{ steps.check.outputs.upstream_sha }} + run: | + TITLE="Sync with upstream ${UPSTREAM_REPO}@${UPSTREAM_SHA:0:7}" + BODY=$(cat < Date: Sat, 23 May 2026 04:31:26 -0400 Subject: [PATCH 2/2] ci: guard against jq 'null' output in sync-upstream workflow gh ... --jq '.[0].number' emits the literal string "null" (not an empty string) when no PRs or issues match, so the existence checks `[ -n "${VAR}" ]` evaluated true and the next call became `gh pr edit null` / `gh issue comment null`, which would fail. Add `// empty` to all three jq expressions so missing values produce no output and the create-vs-edit branches behave correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sync-upstream.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-upstream.yaml b/.github/workflows/sync-upstream.yaml index 3f1c2437..770bc3b2 100644 --- a/.github/workflows/sync-upstream.yaml +++ b/.github/workflows/sync-upstream.yaml @@ -89,7 +89,7 @@ jobs: # (e.g. manual conflict resolution from a previous run), don't # clobber that work. Comment on the PR instead. PR_NUM=$(gh pr list --head "${SYNC_BRANCH}" --base "${FORK_BRANCH}" \ - --state open --json number --jq '.[0].number') + --state open --json number --jq '.[0].number // empty') if [ -n "${PR_NUM}" ] && git rev-parse --verify "origin/${SYNC_BRANCH}" >/dev/null 2>&1; then HUMAN_AHEAD=$(git rev-list --count "HEAD..origin/${SYNC_BRANCH}") if [ "${HUMAN_AHEAD}" -gt 0 ]; then @@ -119,7 +119,7 @@ jobs: EOF ) EXISTING=$(gh pr list --head "${SYNC_BRANCH}" --base "${FORK_BRANCH}" \ - --state open --json number --jq '.[0].number') + --state open --json number --jq '.[0].number // empty') if [ -z "${EXISTING}" ]; then gh pr create --base "${FORK_BRANCH}" --head "${SYNC_BRANCH}" \ --title "${TITLE}" --body "${BODY}" @@ -157,7 +157,7 @@ jobs: ) EXISTING=$(gh issue list --state open --search "Upstream sync conflict in:title" \ --json number,title \ - --jq '[.[] | select(.title | startswith("Upstream sync conflict"))] | .[0].number') + --jq '[.[] | select(.title | startswith("Upstream sync conflict"))] | .[0].number // empty') if [ -z "${EXISTING}" ]; then gh issue create --title "${TITLE}" --body "${BODY}" else