diff --git a/.github/workflows/regenerate.yml b/.github/workflows/regenerate.yml index a50c4d0..facf7cf 100644 --- a/.github/workflows/regenerate.yml +++ b/.github/workflows/regenerate.yml @@ -4,6 +4,13 @@ # the branch `regenerate`, refreshed by each later run until it is merged; # when it changes nothing, an open one is closed. # +# Commits someone adds to that pull request, such as a new service's +# package, are kept: a later run generates on top of them, with main merged +# in, and never closes a pull request that has them. Merging main resolves +# conflicts in what scripts/update.sh writes again anyway (API_REVISION and crates/*/src/generated); +# any other conflict fails the run until someone merges main into the +# branch by hand. +# # The pull request is opened with the workflow token, which starts no # pull_request run, so this dispatches CI on the branch; its result shows on # the pull request's commit. Opening it needs "Allow GitHub Actions to create @@ -37,6 +44,61 @@ jobs: ref: main fetch-depth: 0 + # The open pull request's branch, when someone has added commits to + # it, with main merged in; main otherwise. + - name: Base + run: | + set -euo pipefail + echo "PREVIOUS=$(cat API_REVISION)" >> "$GITHUB_ENV" + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + bot="$(git config user.email)" + base=main + lease="" + # Exit status 2: there is no such branch. Any other failure fails + # the run rather than passing for an absent branch. + found=0 + git ls-remote --exit-code --heads origin "${BRANCH}" >/dev/null || found=$? + if [[ "${found}" -eq 0 ]]; then + git fetch --quiet origin "${BRANCH}" + lease="$(git rev-parse FETCH_HEAD)" + open="$(gh pr list --head "${BRANCH}" --base main --state open --json number,isCrossRepository --jq 'map(select(.isCrossRepository | not))[0].number // empty')" + # Committers too: an amended or rebased commit of the workflow's + # keeps it as the author. + added="$(git log --format='%ae%n%ce' origin/main..FETCH_HEAD | grep -cvxF "${bot}" || true)" + if [[ -n "${open}" && "${added}" -gt 0 ]]; then + git switch --quiet -C "${BRANCH}" FETCH_HEAD + if ! git merge --quiet --no-edit origin/main; then + for path in $(git diff --name-only --diff-filter=U); do + case "${path}" in + API_REVISION | crates/*/src/generated/*) + if git checkout --quiet --ours -- "${path}" 2>/dev/null; then + git add -- "${path}" + else + git rm --quiet -- "${path}" + fi + ;; + esac + done + conflicts="$(git diff --name-only --diff-filter=U | paste -sd ' ' -)" + if [[ -n "${conflicts}" ]]; then + echo "::error::merging main into ${BRANCH} conflicts in ${conflicts}: merge main into it by hand and push" + exit 1 + fi + git commit --quiet --no-edit + fi + base=branch + fi + elif [[ "${found}" -ne 2 ]]; then + echo "::error::could not read origin's branches" + exit 1 + fi + { + echo "BASE=${base}" + echo "LEASE=${lease}" + echo "FROM=$(cat API_REVISION)" + } >> "$GITHUB_ENV" + # rustup installs the toolchain rust-toolchain.toml names. - name: Rust run: rustup toolchain install @@ -45,37 +107,44 @@ jobs: uses: 'Swatinem/rust-cache@v2' - name: Update - run: | - previous="$(cat API_REVISION)" - scripts/update.sh - echo "PREVIOUS=${previous}" >> "$GITHUB_ENV" + run: scripts/update.sh - name: Pull request run: | set -euo pipefail - open="$(gh pr list --head "${BRANCH}" --base main --state open --json number --jq '.[0].number // empty')" - if [[ -z "$(git status --porcelain)" ]]; then + open="$(gh pr list --head "${BRANCH}" --base main --state open --json number,isCrossRepository --jq 'map(select(.isCrossRepository | not))[0].number // empty')" + if [[ "${BASE}" == main && -z "$(git status --porcelain)" ]]; then echo "already current" - if [[ -n "${open}" ]]; then - gh pr close "${open}" --delete-branch --comment "main is current with the API." + # The branch goes only as the Base step found it, and its pull + # request closes with it; one pushed to meanwhile stays. + if [[ -n "${LEASE}" ]]; then + if git push --quiet --force-with-lease="${BRANCH}:${LEASE}" origin ":${BRANCH}"; then + if [[ -n "${open}" ]]; then + gh pr comment "${open}" --body "main is current with the API." + fi + else + echo "::warning::${BRANCH} changed during the run; left as it is" + fi fi exit 0 fi revision="$(cat API_REVISION)" - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git switch --quiet -C "${BRANCH}" - git add --all - git commit --quiet -m "Regenerate from origincloudhq/api@${revision:0:12}" \ - -m "API: https://github.com/origincloudhq/api/compare/${PREVIOUS}...${revision}" + if [[ -n "$(git status --porcelain)" ]]; then + git add --all + git commit --quiet -m "Regenerate from origincloudhq/api@${revision:0:12}" \ + -m "API: https://github.com/origincloudhq/api/compare/${FROM}...${revision}" + fi # Unchanged since the last run: nothing to push or check again. - if git fetch --quiet origin "${BRANCH}" 2>/dev/null && git diff --quiet FETCH_HEAD HEAD; then + if [[ -n "${LEASE}" ]] && git diff --quiet "${LEASE}" HEAD; then echo "pull request #${open:-?} is current" exit 0 fi - git push --quiet --force origin "${BRANCH}" + # Replaces the branch only as this run found it: a commit pushed + # meanwhile fails the run instead of being lost. + git push --quiet --force-with-lease="${BRANCH}:${LEASE}" origin "${BRANCH}" body="Generated from [origincloudhq/api@${revision:0:12}](https://github.com/origincloudhq/api/commit/${revision}) ([changes](https://github.com/origincloudhq/api/compare/${PREVIOUS}...${revision})) by scripts/update.sh.