From 8bb75cf7b9c58a95aaa77649295334057c0310c6 Mon Sep 17 00:00:00 2001 From: Danil Klimuk Date: Thu, 9 Apr 2026 10:07:50 +0200 Subject: [PATCH 01/10] .github: workflows: rebase.yml: add; scripts: rebase.sh: add Signed-off-by: Danil Klimuk --- .github/workflows/rebase.yml | 85 ++++ README.md | 69 +++- scripts/rebase.sh | 776 +++++++++++++++++++++++++++++++++++ 3 files changed, 927 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/rebase.yml create mode 100755 scripts/rebase.sh diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml new file mode 100644 index 0000000..6eede9f --- /dev/null +++ b/.github/workflows/rebase.yml @@ -0,0 +1,85 @@ +--- +name: Try rebasing on updated upstream, report in case of conflicts + +on: + workflow_call: + secrets: + first-remote-token: + description: > + Personal access token for performing the following operations on the + downstream-repo: fetch the repository, create a branch, delete a + branch, create commits on a branch, push to a branch, open a PR, close + a PR, get list of PRs. + required: true + inputs: + downstream-repo: + description: > + parameter for the rebase.sh script. + required: true + type: string + downstream-branch: + description: > + parameter for the rebase.sh script. + required: true + type: string + upstream-repo: + description: > + parameter for the rebase.sh script. + required: true + type: string + upstream-branch: + description: > + parameter for the rebase.sh script. + required: true + type: string + commit-user-name: + description: > + NAME parameter for the --upstream-branch option of the rebase.sh + script. + required: true + type: string + commit-user-email: + description: > + EMAIL parameter for the --commit-user-email option of the rebase.sh + script. + required: true + type: string + cicd-trigger-resume: + description: > + MESSAGE parameter for the --cicd-trigger-resume option of the + rebase.sh script. + required: true + type: string + +jobs: + build-and-package: + runs-on: ubuntu-latest + name: Try rebasing on updated upstream, report in case of conflicts + permissions: + # For creation/deletion/pushing to branches and creating PRs + contents: write + steps: + - uses: actions/checkout@v6 + with: + repository: TrenchBoot/.github + path: shared + ref: master + - name: Run script for rebasing + env: + FIRST_REMOTE_TOKEN: ${{ secrets.first-remote-token }} + DOWNSTREAM_REPO: ${{ inputs.downstream-repo }} + DOWNSTREAM_BRANCH: ${{ inputs.downstream-branch }} + UPSTREAM_REPO: ${{ inputs.upstream-repo }} + UPSTREAM_BRANCH: ${{ inputs.upstream-branch }} + NAME: ${{ inputs.commit-user-name }} + EMAIL: ${{ inputs.commit-user-email }} + MESSAGE: ${{ inputs.cicd-trigger-resume }} + run: | + shared/scripts/rebase.sh --first-remote-token "$FIRST_REMOTE_TOKEN" \ + --commit-user-name "$NAME" \ + --commit-user-email "$EMAIL" \ + --cicd-trigger-resume "$MESSAGE" \ + "$DOWNSTREAM_REPO" \ + "$DOWNSTREAM_BRANCH" \ + "$UPSTREAM_REPO" \ + "$UPSTREAM_BRANCH" diff --git a/README.md b/README.md index 5a49378..0fc7625 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,24 @@ Used by [TrenchBoot/qubes-antievilmaid][aem] and [aem]: https://github.com/TrenchBoot/qubes-antievilmaid/blob/2b6b796e31789fca599986c9cfb0a3ceced5967d/.github/workflows/build.yml [skl]: https://github.com/TrenchBoot/secure-kernel-loader +### rebase + +This workflow automates rebasing a downstream repository branch on top of an +upstream branch. On success, it pushes the rebased branch. If conflicts arise +it, opens a pull request against the downstream repository to ask for +resolution. + +| Parameter | Type | Req. | Def. | Description +| --------- | ---- | ---- | ---- | ----------- +| `downstream-repo` | string | Yes | - | URL of the repository to rebase (`` argument of `rebase.sh`). +| `downstream-branch` | string | Yes | - | Branch in the downstream repository to rebase (`` argument of `rebase.sh`). +| `upstream-repo` | string | Yes | - | URL of the repository that provides the new base (`` argument of `rebase.sh`). +| `upstream-branch` | string | Yes | - | Branch in the upstream repository to rebase onto (`` argument of `rebase.sh`). +| `commit-user-name` | string | Yes | - | Git author name used for rebase commits (`--commit-user-name` option of `rebase.sh`). +| `commit-user-email` | string | Yes | - | Git author e-mail used for rebase commits (`--commit-user-email` option of `rebase.sh`). +| `cicd-trigger-resume` | string | Yes | - | Human-readable message appended to the conflict PR describing how to resume the pipeline (`--cicd-trigger-resume` option of `rebase.sh`). +| `first-remote-token` | string | Yes | - | Personal access token with permissions to fetch, branch, commit, push, and open/close PRs on `downstream-repo`. Passed as a GitHub Actions secret. + ## Usage Full details can be found in [GitHub's documentation][workflow-docs] on @@ -91,12 +109,14 @@ modifications to workflows are necessary. [workflow-docs]: https://docs.github.com/en/actions/using-workflows/reusing-workflows +### qubes-dom0-package or qubes-dom0-packagev2 + Create a workflow file like `.github/workflows/build.yml` inside of your repository. It will have 3 parts: name, triggering conditions and invocation of one of the workflows defined here. Let's use [TrenchBoot/grub][grub] as an example. -### Name +#### Name ```yaml name: Test build and package QubesOS RPMs @@ -104,7 +124,7 @@ name: Test build and package QubesOS RPMs Specify workflow title used for identification in UI. -### Triggering conditions +#### Triggering conditions ```yaml on: @@ -118,7 +138,7 @@ on: Activate this workflow on push of any tag or a branch which starts with `intel-txt-aem` (including this branch, i.e. `*` can expand to an empty string). -### Workflow invocation +#### Workflow invocation ```yaml jobs: @@ -134,6 +154,49 @@ jobs: Invoke v1 workflow from `master` branch of this repository with the set of parameters as described in a section above. +### rebase + +`rebase` is typically one job in a larger workflow that first prepares the +upstream branch to rebase onto, then calls this workflow, and finally cleans up +any temporary branches. + +#### Triggering conditions + +There is no specific trigger condition that can be used to trigger pipelines +that contain this reusable workflow. So the developer is free to decide. But +there is one case: if the workflow that uses this reusable workflow has a +condition on push event, then the token provided via `first-remote-token` should +not have permissions to trigger CI/CDs. This is because the script used inside +this reusable workflow pushes to the remote repository several times. + +#### Workflow invocation + +```yaml +name: Rebase on top of QubesOS main + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 6' + +jobs: + try-rebase: + uses: TrenchBoot/.github/.github/workflows/rebase.yml@master + secrets: + first-remote-token: ${{secrets.TRENCHBOOT_REBASE_TOKEN}} + permissions: + # For creation/deletion/pushing to branches and creating PRs + contents: write + with: + downstream-repo: 'https://github.com/DaniilKl/qubes-antievilmaid.git' + downstream-branch: 'main' + upstream-repo: 'https://github.com/QubesOS/qubes-antievilmaid.git' + upstream-branch: 'main' + commit-user-name: 'github-actions[bot]' + commit-user-email: 'github-actions[bot]@users.noreply.github.com' + cicd-trigger-resume: '7. Rerun the workflow https://github.com/DaniilKl/qubes-antievilmaid/actions/runs/${{ github.run_id }} to resume automated rebase.' +``` + ## Funding This project was partially funded through the diff --git a/scripts/rebase.sh b/scripts/rebase.sh new file mode 100755 index 0000000..12a161c --- /dev/null +++ b/scripts/rebase.sh @@ -0,0 +1,776 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2026 3mdeb +# +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +# Help +usage() { + cat < + +Rebase a branch from the first repo onto a branch from the second repo. On +conflict, stops the rebase and creates a conflict branch containing all commits +that rebased cleanly before the conflict. + +Note, that this script supports only HTTPS protocol for fetching remote +repositories. + +A developer then needs to solve the conflict manually according to the following +steps: +1. (For remote repos only) fetch the remote repository. +2. Enter the repository. +3. Checkout the conflict branch created by the script (check the conflict branch + naming below). +4. Cherry-pick the commit that introduced the conflict (the hash of the commit + will be reported by the script or will be a part of the conflict branch name). +5. Solve the conflict and apply the commit after solving the conflict on top of + the conflict branch (e.g., by using "git cherry-pick --continue"). +6. (For remote repos only) push the remote repository. +7. Try to do the rebase with this script again and wait either for a new + conflict or for the rebase to be finished. + +The steps will be accompanied by commands when printed by script when prompting +for conflict resolution. + +In case the --first-remote-token option is provided - the script will do the +following actions on the remote part of the first repository: +* Fetch the first repository using the TOKEN. The second repository will be + fetched using HTTPS without tokens or credentials. +* Will push and delete pushed by this script branches using the TOKEN on the + remote repository. The script will push and delete only the conflict branches. + And when the rebase will succeed it will push a branch with rebased commits + from the on top of the named + '-rebased'. +* Will create a PR from a conflict branch to the in case of + conflicts. + +Currently supported remotes: +* GitHub. + +Arguments: + first_repo HTTPS URL of the fork repository to clone, or path to the local + repository + first_repo_branch + Branch in the fork to rebase + second_repo HTTPS URL of the upstream repository to clone + second_repo_branch + Branch in the upstream to rebase onto + +Options: + -h, --help Show this help message and exit + -l, --local Treat the as local directory path and + skips . + --first-remote-token TOKEN + Access token for the user to access the first repository on + the remote. + --commit-user-name NAME + The name used for "git config user.name". If not provided - + the "github-actions[bot]" is being used as the default name. + This name is used when creating new commits during rebase. + --commit-user-email EMAIL + The email used for "git config user.email". If not provided - + the "github-actions[bot]@users.noreply.github.com" is being + used as default email. This email is used when creating new + commits during rebase. + --cicd-trigger-resume MESSAGE + When using the script with --first-remote-token, the script + creates a PR on the remote part of the with a + comment on how to resolve the conflict correctly. But by + default, the comment does not contain a message on how to + resume the automatic rebase with this script when the script + is used in CI/CD. The reason this information is missing is + that this script should not depend by default on whether + it is launched in CI/CD or not, hence this script by default + expects that it will be launched from a CLI. Hence, CI/CD + launches this script as a step in a job as a BASH script. + Hence, it is for the CI/CD configuration to determine when + the CI/CD is triggered. It might be when a developer pushes + to the conflict branch, closes the created by this script + PR, etc. So the CI/CD configuration can communicate to the + developer via the MESSAGE how to relaunch automatic rebase + after resolving the conflict. + -v, --verbose Print a lot of debug information. + +Conflict branch naming: + -<40-char-hash-of-conflicting-commit>-conflict + +Exit codes: + 0 Rebase completed successfuly + 1 Some other issue encountered + 2 Conflict encountered (conflict branch created) + 3 Script logic failure + 4 Multiple conflict branches found + 5 No rebase needed + 6 The last successful rebase has not been managed properly. + +The error code "4" means the git history of the first repository contains +several branches that match the conflict branch naming described above, but the +names differ by the commit hash. Script uses the commit hash as a base for +creating commits during rebase, and when it sees several commit hashes, it +cannot continue as it does not have any logic to decide which commit hash to +use. In such a case, the developer should either delete all the conflict +branches and start the rebase with this script over, or delete all the conflict +branches except the correct one and try to continue rebasing with this script. + +Example: + $(basename "$0") \\ + https://github.com/you/my-fork.git my-feature \\ + https://github.com/org/upstream.git main +EOF +} + +# This function pushes a branch to a remote repository. Return codes: +# 0: Success. +# 1: Some issue. +push_branch_remote() { + local token="${1:-}" + local branch="${2:-}" + local remote="${3:-}" + + # The remote URL must contain the token for the ref to be modified on the + # remote via personal access token authentication: + git remote get-url "$remote" 2> /dev/null | grep -F "$token" &> /dev/null || return 1 + git push "$remote" "$branch" &> /dev/null || return 1 + + return 0 +} + +# This function deletes a branch on a remote repository. Return codes: +# 0: Success. +# 1: Some issue. +# 2: The function tried to delete a branch that was not created by this script +# and probably belongs to somebody else. +delete_branch_remote() { + local token="${1:-}" + local branch="${2:-}" + local remote="${3:-}" + local temp="" + local commit="" + + # Some checks to make sure that we are deleting the branch created by this + # script and not some other branch: + # 1. The branch must match the pattern for branches with conflicts that are + # created by this script: + echo "$branch" | grep -E '.*-[a-z0-9]{40}-conflict' &> /dev/null || return 2 + # 2. The branch name must contain a hash of existing commit + temp="${branch%-conflict}" + commit="${temp##*-}" + git show "$commit" &> /dev/null || return 2 + # The checks above are reasonable but not sufficient, as there is a + # probability that a branch that will match the pattern will be created by a + # user. At git level we do not have access to the information on who created + # the branch. But if we have access the following check could be implemented + # (the "could be" means it has not been tested yet): + # + # curl -H "Authorization: Bearer $token" \ + # "https://api.github.com/orgs//audit-log?phrase=create+branch" + # + # This check seems to require organization level access for the token. + + # The remote URL must contain the token for the ref to be modified on the + # remote via personal access token authentication: + git remote get-url "$remote" 2> /dev/null | grep -F "$token" &> /dev/null || return 1 + git push "$remote" --delete "$branch" &> /dev/null || return 1 + + return 0 +} + +# This function creates a PR on the remote repository. Return codes: +# 0: Success. +# 1: Some issue. +# 2: PR was not created. +create_pr_remote() { + local token="${1:-}" + local repo_url="${2:-}" + local head_branch="${3:-}" + local base_branch="${4:-}" + local pr_title="${5:-}" + local pr_body="${6:-}" + local payload repo_path owner repo_name response http_code pr_url body + + + # Derive owner/repo from the URL (supports + # https://github.com/OWNER/REPO[.git]): + repo_path="$(echo "$repo_url" | sed -E 's|.*github\.com[:/]||; s|\.git$||')" + owner="$(cut -d'/' -f1 <<< "$repo_path")" + repo_name="$(cut -d'/' -f2 <<< "$repo_path")" + local api_url="https://api.github.com/repos/${owner}/${repo_name}/pulls" + + if [[ -z "$owner" || -z "$repo_name" ]]; then + echo "ERROR: Could not parse owner/repo from URL: $repo_url" >&2 + return 1 + fi + + if [[ -z "$pr_title" || -z "$pr_body" ]]; then + echo "ERROR: No PR title and/or PR body provided" >&2 + return 1 + fi + + if command -v jq &>/dev/null; then + payload="$(jq -n \ + --arg title "$pr_title" \ + --arg head "$head_branch" \ + --arg base "$base_branch" \ + --arg body "$pr_body" \ + '{title: $title, head: $head, base: $base, body: $body}')" + elif command -v python3 &>/dev/null; then + payload="$(python3 -c " +import json, sys +print(json.dumps({ + 'title': sys.argv[1], + 'head': sys.argv[2], + 'base': sys.argv[3], + 'body': sys.argv[4], +}))" "$pr_title" "$head_branch" "$base_branch" "$pr_body")" + else + echo "ERROR: jq or python3 is required to work on the JSON payload." >&2 + return 1 + fi + + response="$(curl -s -w "\n%{http_code}" \ + -X POST "$api_url" \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${token}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + -H "Content-Type: application/json" \ + -d "$payload")" + + http_code="$(tail -n1 <<< "$response")" + body="$(head -n -1 <<< "$response")" + + if [[ "$http_code" != "201" ]]; then + echo "ERROR: GitHub API returned HTTP ${http_code}:" >&2 + echo "$response" >&2 + return 1 + fi + + if command -v jq &>/dev/null; then + pr_url="$(jq -r '.html_url' <<< "$body")" + elif command -v python3 &>/dev/null; then + pr_url="$(python3 -c "import json,sys; print(json.load(sys.stdin)['html_url'])" <<< "$body")" + else + echo "ERROR: jq or python3 is required to work on the JSON payload." >&2 + return 1 + fi + + echo "Pull request created: ${pr_url}" + + return 0 +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + -v|--verbose) + set -x + shift + ;; + -h|--help) + usage + exit 0 + ;; + -l|--local) + LOCAL="true" + shift + ;; + --first-remote-token) + TOKEN="$2" + shift 2 + ;; + --commit-user-name) + COMMIT_USER_NAME="$2" + shift 2 + ;; + --commit-user-email) + COMMIT_USER_EMAIL="$2" + shift 2 + ;; + --cicd-trigger-resume) + CICD_TRIGGER_RESUME="$2" + shift 2 + ;; + -*) + echo "ERROR: Unknown option $1" >&2 + usage + exit 1 + ;; + *) + POSITIONAL_ARGS+=( "$1" ) + shift + ;; + esac + done +} + +# This function prepares remote repository URL for usage via GitHub's personal +# access token authentication. Return codes: +# 0: Success. +# 1: No HTTPS protocol prefix found in the remote repository URL. +build_url_with_token() { + local url="${1:-}" + local token="${2:-}" + local repo_path + + # The limit on HTTPS only is because of the personal access token that is + # used here and works only over HTTPS: + echo "$url" | grep 'https://' &> /dev/null || return 1 + + repo_path="$(echo "$url" | sed -E 's|.*github\.com[:/]||; s|\.git$||')" + echo "https://$token@github.com/$repo_path.git" 2> /dev/null + + return 0 +} + +# This function checks whether a rebase is needed. Return codes: +# 0: Rebase is needed. +# 1: Rebase is not needed. +check_for_rebase() { + local head_ref="${1:-}" + local newbase="${2:-}" + local result + + result="$(git log "$head_ref".."$newbase" --oneline 2> /dev/null)" + + if [[ -z "$result" ]]; then + return 1 + fi + + return 0 +} + +# This function checks whether a PR from branch A to branch B exists and is open +# on remote. Return codes: +# 0: Does exist. +# 1: Does not exist. +check_for_pr() { + local token="${1:-}" + local repo_url="${2:-}" + local branch1="${3:-}" + local branch2="${4:-}" + local result=1 + local response repo_path owner repo_name + + # Derive owner/repo from the URL (supports + # https://github.com/OWNER/REPO[.git]): + repo_path="$(echo "$repo_url" | sed -E 's|.*github\.com[:/]||; s|\.git$||')" + owner="$(cut -d'/' -f1 <<< "$repo_path")" + repo_name="$(cut -d'/' -f2 <<< "$repo_path")" + local api_url="https://api.github.com/repos/${owner}/${repo_name}/pulls" + + response="$(curl -s -H "Authorization: Bearer $token" \ + "$api_url?head=$owner:$branch1&base=$branch2&state=open")" + + # The conclusion is based on the response body length. If the response + # contains any objects - PR exists, if not - PR does not exist. + if command -v jq &>/dev/null; then + result="$(jq 'length' <<< "$response")" + elif command -v python3 &>/dev/null; then + result="$(python3 -c "import sys, json; print(len(json.load(sys.stdin)))" <<< "$response")" + fi + + if [ $result -eq 0 ]; then + return 1 + fi + + return 0 +} + +# This function checks if conflict has been resolved and the commit with the +# resolved conflict is present on the conflict branch. Return codes: +# 0: The conflict is resolved and the commit is present. +# 1: The conflict is not resolved or the commit is not present. Or in some other +# undefined state +check_if_resolved() { + local head_ref=${1:-} + local base=${2:-} + local newbase=${3:-} + local commits1 commits1_num commits2 commits2_num + + commits1="$(git log "$newbase".."$head_ref" --oneline)" + commits2="$(git log "$newbase".."$base" --oneline)" + + commits1_num=$(printf '%s' "$commits1" | wc -l) + commits2_num=$(printf '%s' "$commits2" | wc -l) + + if [[ $commits1_num -eq $commits2_num ]]; then + return 0 + fi + + return 1 +} + +# Configuration and initial values: +declare -a BRANCHES +declare BRANCH_TEMP +LOCAL="" +TOKEN="" +BRANCH="" +COMMIT="" +COMMIT_USER_NAME="github-actions[bot]" +COMMIT_USER_EMAIL="github-actions[bot]@users.noreply.github.com" +CICD_TRIGGER_RESUME="" +REBASE_HEAD_FILE=".git/REBASE_HEAD" + + +POSITIONAL_ARGS=() +parse_args "$@" +set -- "${POSITIONAL_ARGS[@]}" + +FIRST_REPO="$1" +FIRST_REPO_BRANCH="$2" +FIRST_REPO_REMOTE_NAME="origin" +if [[ -z "$LOCAL" ]]; then + if [[ "${#POSITIONAL_ARGS[@]}" -ne "4" ]]; then + usage + exit 1 + fi + SECOND_REPO="$3" + SECOND_REPO_BRANCH="$4" + SECOND_REPO_REMOTE_NAME="second-repo" + SECOND_REPO_REF="$SECOND_REPO_REMOTE_NAME/$SECOND_REPO_BRANCH" + WORK_DIR=$(mktemp -d) + REPO_DIR="$WORK_DIR/repo" +else + if [[ "${#POSITIONAL_ARGS[@]}" -ne "3" ]]; then + usage + exit 1 + fi + SECOND_REPO_BRANCH="$3" + SECOND_REPO_REF="$SECOND_REPO_BRANCH" + REPO_DIR="$FIRST_REPO" +fi + +SUCCESSULL_REBASE_PR_TITLE="Automatic rebase of branch $FIRST_REPO_BRANCH completed successfuly" +SUCCESSULL_REBASE_MESSAGE=" +Summary: +* Rebased branch $FIRST_REPO_BRANCH from repository $FIRST_REPO." + +if [[ -z "$LOCAL" ]]; then + SUCCESSULL_REBASE_MESSAGE+=" +* New base: $SECOND_REPO_BRANCH from repository $SECOND_REPO." +else + SUCCESSULL_REBASE_MESSAGE+=" +* New base: $SECOND_REPO_BRANCH from repository $FIRST_REPO." +fi + +SUCCESSULL_REBASE_MESSAGE+=" + +Please, manage the rebased branch by either merging $FIRST_REPO_BRANCH-rebased +into $FIRST_REPO_BRANCH, force pushing branch $FIRST_REPO_BRANCH to include +commits from $FIRST_REPO_BRANCH-rebased, or any other way suitable for this +repository. + +Delete the branch $FIRST_REPO_BRANCH-rebased after you are done. +" + +echo "Working directory: $REPO_DIR" + +################################################################################ +# Repositories preparation +################################################################################ +# Clone first repo and checkout branch: +if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then + repo_path="$(build_url_with_token "$FIRST_REPO" "$TOKEN")" + + echo "Cloning the first repository: $FIRST_REPO..." + git clone "$repo_path" "$REPO_DIR" &> /dev/null + cd "$REPO_DIR" &> /dev/null + git fetch --all &> /dev/null + cd - &> /dev/null + unset repo_path +elif [[ "$LOCAL" != "true" ]]; then + echo "Cloning the first repository: $FIRST_REPO..." + git clone "$FIRST_REPO" "$REPO_DIR" &> /dev/null +fi + +echo "Checking out branch '$FIRST_REPO_BRANCH'..." +cd "$REPO_DIR" &> /dev/null +git checkout "$FIRST_REPO_BRANCH" &> /dev/null + +echo "Setting user name to $COMMIT_USER_NAME for commits..." +git config user.name "$COMMIT_USER_NAME" &> /dev/null +echo "Setting user email to $COMMIT_USER_EMAIL for commits..." +git config user.email "$COMMIT_USER_EMAIL" &> /dev/null + +# Add second repo as a remote: +if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then + echo "Adding second repo as a remote $SECOND_REPO..." + git remote add "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO" &> /dev/null + unset repo_path + + echo "Fetching from the second repo branch '$SECOND_REPO_BRANCH'..." + git fetch "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO_BRANCH" &> /dev/null +elif [[ "$LOCAL" != "true" ]]; then + echo "Adding second repo as a remote $SECOND_REPO..." + git remote add "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO" &> /dev/null + + echo "Fetching from the second repo '$SECOND_REPO_BRANCH'..." + git fetch "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO_BRANCH" &> /dev/null +fi + +################################################################################ +# Rebasing decision logic +################################################################################ +# Check if there is a FIRST_REPO_BRANCH-rebased branch. If yes - do not start a +# new rebase, as the last successful rebase was not managed properly. +if [[ "$LOCAL" != "true" ]]; then + BRANCH_TEMP=$(git branch -r --no-column --list "$FIRST_REPO_REMOTE_NAME"/"$FIRST_REPO_BRANCH"-rebased 2> /dev/null) +else + BRANCH_TEMP=$(git branch --no-column --list "$FIRST_REPO_BRANCH"-rebased 2> /dev/null) +fi + +if echo "$BRANCH_TEMP" | grep rebased &> /dev/null; then + echo "The last successful rebase of the branch $FIRST_REPO_BRANCH is still +present in the repository history. Please merge or delete it and restart the +automatic rebase." + exit 6 +fi +unset BRANCH_TEMP + +# Check state we are in. Checks, if this is the first rebase attempt or a +# consequently triggered after a manual conflict resolution rebase attempt. +# Search for a previous branch with a conflict: +if [[ "$LOCAL" != "true" ]]; then + BRANCH_TEMP=$(git branch -r --no-column --list "$FIRST_REPO_REMOTE_NAME"/"$FIRST_REPO_BRANCH"-*-conflict 2> /dev/null) +else + BRANCH_TEMP=$(git branch --no-column --list "$FIRST_REPO_BRANCH"-*-conflict 2> /dev/null) +fi + +while IFS= read -r line; do + BRANCHES+=("$line") +done <<< "$BRANCH_TEMP" +unset BRANCH_TEMP + +if [[ "${#BRANCHES[@]}" == "1" && -n "${BRANCHES[0]}" ]]; then + if [[ "$LOCAL" != "true" ]]; then + BRANCH="${BRANCHES[0]##*/}" + git checkout -b "$BRANCH" "$FIRST_REPO_REMOTE_NAME/$BRANCH" + else + BRANCH="${BRANCHES[0]##* }" + fi + echo "Continuing rebase of the branch '$FIRST_REPO_BRANCH' from the last commit in branch '$BRANCH'..." + temp="${BRANCH%-conflict}" + COMMIT="${temp##*-}" +elif [[ "${#BRANCHES[@]}" == "1" && -z "${BRANCHES[0]}" ]]; then + echo "Starting a new rebase..." +else + echo "ERROR: Repository has several conflict branches for the '$FIRST_REPO_BRANCH' and needs cleanup, exiting..." >&2 + exit 4 +fi +unset BRANCHES + +################################################################################ +# Attempt/continue rebase +################################################################################ +if [[ -z "$COMMIT" && -z "$BRANCH" ]]; then + if ! check_for_rebase "$FIRST_REPO_BRANCH" "$SECOND_REPO_REF"; then + echo "Current branch $FIRST_REPO_BRANCH is up to date with $SECOND_REPO_REF." + exit 5 + fi + + echo "Rebasing '$FIRST_REPO_BRANCH' onto '$SECOND_REPO_REF'..." + + if git rebase "$SECOND_REPO_REF" &> /dev/null; then + echo "Rebase completed successfuly. No conflicts." + + # Do not push to the same branch on the remote repository to avoid + # force pushes: + git checkout -b "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" &> /dev/null + if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then + push_branch_remote "$TOKEN" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_REMOTE_NAME" + if ! check_for_pr "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" ; then + create_pr_remote "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" "$SUCCESSULL_REBASE_PR_TITLE" "$SUCCESSULL_REBASE_MESSAGE" || exit 1 + fi + else + echo "$SUCCESSULL_REBASE_MESSAGE" + fi + exit 0 + fi +elif [[ -n "$COMMIT" && -n "$BRANCH" ]]; then + if ! check_if_resolved "$BRANCH" "$COMMIT" "$SECOND_REPO_REF"; then + echo "ERROR: still a conflict." >&2 + exit 2 + fi + + echo "Continuing rebase '$FIRST_REPO_BRANCH' onto '$BRANCH' using commit $COMMIT as a base..." + + if git rebase --onto "$BRANCH" "$COMMIT" "$FIRST_REPO_BRANCH" &> /dev/null; then + # Delete the temporary conflict branch so there is no leftovers after a + # success: + git branch --delete "$BRANCH" &> /dev/null + git checkout -b "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" &> /dev/null + + if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then + # Delete the temporary conflict branch on remote so there is no + # leftovers after a success: + delete_branch_remote "$TOKEN" "$BRANCH" "$FIRST_REPO_REMOTE_NAME" + + # Do not push to the same branch on the remote repository to avoid + # force pushes: + push_branch_remote "$TOKEN" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_REMOTE_NAME" + if ! check_for_pr "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" ; then + create_pr_remote "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" "$SUCCESSULL_REBASE_PR_TITLE" "$SUCCESSULL_REBASE_MESSAGE" || exit 1 + fi + else + echo "$SUCCESSULL_REBASE_MESSAGE" + fi + + echo "Rebase completed successfuly. No new conflicts." + exit 0 + fi +else + echo "ERROR: Oh no, something went wrong! The script cannot continue the rebase." >&2 + exit 3 +fi + +################################################################################ +# Conflict handling: +################################################################################ +# The strategy: never try to resolve conflicts on your own, ask a developer +# instead. +echo "Conflict detected. Inspecting rebase state..." + +if [[ ! -f "$REBASE_HEAD_FILE" ]]; then + echo "ERROR: Expected .git/REBASE_HEAD not found. Cannot determine conflicting commit." >&2 + git rebase --abort &> /dev/null + exit 3 +fi + +CONFLICT_COMMIT=$(cat "$REBASE_HEAD_FILE" 2> /dev/null) + +# Build the conflict branch name: --conflict +CONFLICT_BRANCH="${FIRST_REPO_BRANCH}-${CONFLICT_COMMIT}-conflict" + + +# Create a branch at HEAD (last successfuly rebased commit) before aborting to +# save the current state of the rebase. Delete the previous temporary conflict +# branch to prevent the situation that cause this script to return code 4 (see +# the usage). If the branch that was used during the rebase has the same name as +# the CONFLICT_BRANCH - it means that the conflict was either not resolved nor +# pushed to the branch after resolution by the developer. Hence, no need to +# create a branch. +if [[ "$BRANCH" != "$CONFLICT_BRANCH" ]]; then + git branch "$CONFLICT_BRANCH" &> /dev/null + if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then + push_branch_remote "$TOKEN" "$CONFLICT_BRANCH" "$FIRST_REPO_REMOTE_NAME" || exit 1 + fi + + if [[ -n "$BRANCH" ]]; then + git branch --delete "$BRANCH" &> /dev/null + if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then + delete_branch_remote "$TOKEN" "$BRANCH" "$FIRST_REPO_REMOTE_NAME" + fi + fi +fi + +git rebase --abort &> /dev/null + +################################################################################ +# Opening a PR/communicating via CLI with instructions on how to proceed: +################################################################################ +message="Automatic rebase of branch '$FIRST_REPO_BRANCH' met a conflict. + +Summary: +* First repo : $FIRST_REPO +* First repo branch : $FIRST_REPO_BRANCH +" +if [[ "$LOCAL" != "true" ]]; then + message+=" +* Second repo : $SECOND_REPO +* Second repo branch : $SECOND_REPO_BRANCH" +fi +message+=" +* Branch with the successfuly rebased commits : $CONFLICT_BRANCH +* The commit that introduced the conflict : $CONFLICT_COMMIT + +Before relaunching the automatic rebase, please do the following to solve the +conflict:" + +if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then +message+=" +1. Fetch the remote repository: + + \`\`\` + git clone $FIRST_REPO + \`\`\` + +2. Enter the repository. +3. Checkout the conflict branch created by the script: + + \`\`\` + git checkout $CONFLICT_BRANCH + \`\`\` + +4. Cherry-pick the commit that introduced the conflict + + \`\`\` + git cherry-pick $CONFLICT_COMMIT + \`\`\` + +5. Solve the conflict and apply the commit after solving the conflict on top of + the conflict branch: + + \`\`\` + git add . + git cherry-pick --continue + \`\`\` + +6. Push the remote repository. + + \`\`\` + git push origin $CONFLICT_BRANCH + \`\`\` + +" + + if [[ -n "$CICD_TRIGGER_RESUME" ]]; then + message+="$CICD_TRIGGER_RESUME" + fi + + message+=" + +If you want to start the automatic rebase from the beginning, then make sure to: + +* Remove the $CONFLICT_BRANCH from the remote repository. +* Close this PR. +" +else +message+=" +1. Enter the repository. +2. Checkout the conflict branch created by the script: + + git checkout $CONFLICT_BRANCH + +3. Cherry-pick the commit that introduced the conflict + + git cherry-pick $CONFLICT_COMMIT + +4. Solve the conflict and apply the commit after solving the conflict on top of + the conflict branch: + + git add . + git cherry-pick --continue + +5. Try to do the rebase with this script again and wait either for a new + conflict or for the rebase to be finished, e.g.: + + ./rebase.sh --local $REPO_DIR $FIRST_REPO_BRANCH $SECOND_REPO_BRANCH +" +fi + + +if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then + pr_body="$message" + pr_title="Automatic rebase of branch '$FIRST_REPO_BRANCH' met a conflict." + + if ! check_for_pr "$TOKEN" "$FIRST_REPO" "$CONFLICT_BRANCH" "$FIRST_REPO_BRANCH" ; then + create_pr_remote "$TOKEN" "$FIRST_REPO" "$CONFLICT_BRANCH" "$FIRST_REPO_BRANCH" "$pr_title" "$pr_body" || exit 1 + fi +else + echo "$message" +fi + +exit 2 From 58e44a127032c07bcfca28cb3c051e6d7cfa72ba Mon Sep 17 00:00:00 2001 From: Danil Klimuk Date: Mon, 13 Apr 2026 14:59:49 +0200 Subject: [PATCH 02/10] Add CI/CD workflow and a script for triggering Woodpecker workflow via woodpecker-cli Signed-off-by: Danil Klimuk --- .../workflows/trigger-woodpecker-pipeline.yml | 73 ++++++ README.md | 68 ++++++ scripts/apply-qubes-patches.sh | 170 ++++++++++++++ scripts/woodpecker-trigger.sh | 220 ++++++++++++++++++ 4 files changed, 531 insertions(+) create mode 100644 .github/workflows/trigger-woodpecker-pipeline.yml create mode 100755 scripts/apply-qubes-patches.sh create mode 100755 scripts/woodpecker-trigger.sh diff --git a/.github/workflows/trigger-woodpecker-pipeline.yml b/.github/workflows/trigger-woodpecker-pipeline.yml new file mode 100644 index 0000000..dc387cc --- /dev/null +++ b/.github/workflows/trigger-woodpecker-pipeline.yml @@ -0,0 +1,73 @@ +name: Trigger a Woodpecker CI/CD pipeline + +on: + workflow_call: + inputs: + api-url: + description: > + Base URL of the Woodpecker instance, e.g. https://ci.example.com. + --api-url parameter for the woodpecker-trigger.sh script. + required: true + type: string + owner: + description: > + Repository owner (user or organization). + --owner parameter for the woodpecker-trigger.sh script. + required: true + type: string + repo: + description: > + Repository name. + --repo parameter for the woodpecker-trigger.sh script. + required: true + type: string + ref: + description: > + Branch to trigger the pipeline on. + --ref parameter for the woodpecker-trigger.sh script. + required: false + type: string + default: 'main' + inputs: + description: > + Additional --input flags to pass to woodpecker-trigger.sh, e.g. + "--input KEY=VALUE --input KEY2=VALUE2". Keys must be valid shell + variable names (no hyphens). + required: false + type: string + default: '' + secrets: + woodpecker-token: + description: > + Woodpecker API token for triggering the pipeline. + --token parameter for the woodpecker-trigger.sh script. + required: true + +jobs: + trigger-woodpecker: + runs-on: ubuntu-latest + name: Trigger a Woodpecker CI/CD pipeline + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + repository: TrenchBoot/.github + path: shared + ref: master + - name: Trigger Woodpecker CI/CD pipeline + env: + WOODPECKER_TOKEN: ${{ secrets.woodpecker-token }} + WOODPECKER_API_URL: ${{ inputs.api-url }} + WOODPECKER_OWNER: ${{ inputs.owner }} + WOODPECKER_REPO: ${{ inputs.repo }} + WOODPECKER_REF: ${{ inputs.ref }} + WOODPECKER_INPUTS: ${{ inputs.inputs }} + run: | + shared/scripts/woodpecker-trigger.sh \ + --token "$WOODPECKER_TOKEN" \ + --api-url "$WOODPECKER_API_URL" \ + --owner "$WOODPECKER_OWNER" \ + --repo "$WOODPECKER_REPO" \ + --ref "$WOODPECKER_REF" \ + $WOODPECKER_INPUTS diff --git a/README.md b/README.md index 0fc7625..712c4cc 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,22 @@ resolution. | `cicd-trigger-resume` | string | Yes | - | Human-readable message appended to the conflict PR describing how to resume the pipeline (`--cicd-trigger-resume` option of `rebase.sh`). | `first-remote-token` | string | Yes | - | Personal access token with permissions to fetch, branch, commit, push, and open/close PRs on `downstream-repo`. Passed as a GitHub Actions secret. +### trigger-woodpecker-pipeline + +This workflow is a generic wrapper for the woodpecker-trigger.sh script for +triggering Woodpecker CI/CD pipelines on some remote Woodpecker instance. As for +now it is used only for triggering the pipelines for signing RPM packages built +by the `qubes-dom0-package` and `qubes-dom0-packagev2` workflows. + +| Parameter | Type | Req. | Def. | Description +| --------- | ---- | ---- | ---- | ----------- +| `api-url` | string | Yes | - | Base URL of the Woodpecker instance, e.g. `https://ci.example.com`. +| `owner` | string | Yes | - | Repository owner (user or organization). +| `repo` | string | Yes | - | Repository name. +| `ref` | string | No | `main` | Branch to trigger the pipeline on. +| `inputs` | string | No | - | Additional `--input KEY=VALUE` flags passed to `woodpecker-trigger.sh`. Keys must be valid shell variable names (no hyphens). +| `woodpecker-token` | string | Yes | - | Woodpecker API token for authentication. Passed as a GitHub Actions secret. + ## Usage Full details can be found in [GitHub's documentation][workflow-docs] on @@ -197,6 +213,58 @@ jobs: cicd-trigger-resume: '7. Rerun the workflow https://github.com/DaniilKl/qubes-antievilmaid/actions/runs/${{ github.run_id }} to resume automated rebase.' ``` +### trigger-woodpecker-pipeline + +`trigger-woodpecker-pipeline` is meant to be added as an additional job to an +existing workflow, chained after a `qubes-dom0-package` or `qubes-dom0-packagev2` +job. + +#### Workflow invocation + +An example invocation: + +```yaml +jobs: + qubes-dom0-package: + needs: get-version + uses: TrenchBoot/.github/.github/workflows/qubes-dom0-packagev2.yml@master + with: + qubes-component: 'vmm-xen' + qubes-component-branch: 'aem-next-rebased' + qubes-pkg-src-dir: '.' + qubes-pkg-version: '4.19.4' + trigger-woodpecker-cicd: + needs: qubes-dom0-package + uses: TrenchBoot/.github/.github/workflows/trigger-woodpecker-pipeline.yml@master + secrets: + woodpecker-token: ${{ secrets.WOODPECKER_TOKEN }} + with: + api-url: 'https://ci.3mdeb.com' + owner: 'zarhus' + repo: 'trenchboot-release-cicd-pipeline' + ref: 'master' + inputs: >- + --input GITHUB_REPO=xen + --input GITHUB_SHA=${{ github.sha }} + --input GITHUB_RUN_ID=${{ github.run_id }} + --input QUBES_COMPONENT=vmm-xen + --input WORKFLOW=sign-and-publish-test-rpms +``` + +Invokes the workflow from `master` branch of this repository after the +`qubes-dom0-package` job completes. Pass the Woodpecker API token from the +repository's GitHub secrets, point it at the target Woodpecker instance +and repository, and supply any pipeline-specific key/value pairs via repeated +`--input` flags. + +Note, that all the inputs to the `trigger-woodpecker-pipeline.yml` except from +the `inputs` serve for the purpose of connection to the desired Woodpecker +instance on which a pipeline for signing is running. But the data provided via +`inputs` input and `--input` flag is consumed by the signing pipeline itself. +One must specify the name of the signing pipeline via `--input WORKFLOW=` and +all the input data the specified pipeline requires. The above example presents +the required inputs for the `sign-and-publish-test-rpms` pipeline. + ## Funding This project was partially funded through the diff --git a/scripts/apply-qubes-patches.sh b/scripts/apply-qubes-patches.sh new file mode 100755 index 0000000..fba1a27 --- /dev/null +++ b/scripts/apply-qubes-patches.sh @@ -0,0 +1,170 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2026 3mdeb +# +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +usage() { + cat <&2 + usage >&2 + exit 1 + ;; + *) + echo "ERROR: Unexpected argument '$1'" >&2 + usage >&2 + exit 1 + ;; + esac + done +} + +parse_args "$@" + +# Validate required parameters: +errors=0 +[[ -z "$PATCHES_REPO" ]] && { echo "ERROR: --patches-repo is required." >&2; errors=$((errors+1)); } +[[ -z "$DOWNSTREAM_REPO" ]] && { echo "ERROR: --downstream-repo is required." >&2; errors=$((errors+1)); } +[[ -z "$UPSTREAM_URL" ]] && { echo "ERROR: --upstream-url is required." >&2; errors=$((errors+1)); } +[[ -z "$UPSTREAM_TAG_PREFIX" ]] && { echo "ERROR: --upstream-tag-prefix is required." >&2; errors=$((errors+1)); } +[[ -z "$RESULT_BRANCH" ]] && { echo "ERROR: --result-branch is required." >&2; errors=$((errors+1)); } +[[ -z "$TOKEN" ]] && { echo "ERROR: --token is required." >&2; errors=$((errors+1)); } +if [[ $errors -gt 0 ]]; then + usage >&2 + exit 1 +fi + +# Clone the patches repository: +echo "Cloning patches repository ${PATCHES_REPO}..." +git clone "https://github.com/${PATCHES_REPO}.git" patches-repo + +# Read the upstream version from the patches repository's version file: +VERSION="$(tr -d '[:space:]' < patches-repo/version)" +UPSTREAM_TAG="${UPSTREAM_TAG_PREFIX}${VERSION}" +echo "Upstream version: ${VERSION}, tag: ${UPSTREAM_TAG}" + +# Clone the downstream repository using the provided token: +echo "Cloning downstream repository ${DOWNSTREAM_REPO}..." +git clone "https://x-access-token:${TOKEN}@github.com/${DOWNSTREAM_REPO}.git" downstream-repo + +cd downstream-repo + +# Add the upstream remote and fetch only the required tag: +echo "Fetching upstream tag ${UPSTREAM_TAG} from ${UPSTREAM_URL}..." +git remote add upstream "$UPSTREAM_URL" +git fetch upstream "refs/tags/${UPSTREAM_TAG}:refs/tags/${UPSTREAM_TAG}" + +# Configure the git identity used for the commits created by git am: +git config user.name 'github-actions[bot]' +git config user.email 'github-actions[bot]@users.noreply.github.com' + +# Create the result branch at the upstream tag and apply the patches: +echo "Creating branch '${RESULT_BRANCH}' at tag '${UPSTREAM_TAG}'..." +git checkout -b "$RESULT_BRANCH" "$UPSTREAM_TAG" + +echo "Applying patches from ${PATCHES_REPO}..." +git am ../patches-repo/*.patch + +# Force-push the result branch to the downstream repository so that re-runs +# are idempotent: +echo "Pushing '${RESULT_BRANCH}' to ${DOWNSTREAM_REPO}..." +git push --force origin "$RESULT_BRANCH" + +echo "Done. Branch '${RESULT_BRANCH}' pushed to ${DOWNSTREAM_REPO}." diff --git a/scripts/woodpecker-trigger.sh b/scripts/woodpecker-trigger.sh new file mode 100755 index 0000000..e18b073 --- /dev/null +++ b/scripts/woodpecker-trigger.sh @@ -0,0 +1,220 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2026 3mdeb +# +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +# Pinned woodpecker-cli version. Override with the WOODPECKER_CLI_VERSION +# environment variable. +# Check https://github.com/woodpecker-ci/woodpecker/releases for available +# versions. +WOODPECKER_CLI_VERSION="${WOODPECKER_CLI_VERSION:-3.13.0}" +WP_BIN="" +WP_TMPDIR="" + +usage() { + cat <&2 + return 1 + ;; + esac +} + +# Prints the architecture component of the woodpecker-cli binary name. +# Return codes: +# 0: Success. +# 1: Unsupported architecture. +detect_arch() { + case "$(uname -m)" in + x86_64) echo "amd64" ;; + aarch64|arm64) echo "arm64" ;; + armv7l) echo "arm-7" ;; + i386|i686) echo "386" ;; + *) + echo "ERROR: Unsupported architecture: $(uname -m)" >&2 + return 1 + ;; + esac +} + +# Downloads the woodpecker-cli binary for the current platform to a temporary +# directory. Sets the global WP_BIN and WP_TMPDIR variables. +# Return codes: +# 0: Success. +# 1: Download or platform detection failed. +download_woodpecker_cli() { + local version="$1" + local os arch filename url + + os="$(detect_os)" + arch="$(detect_arch)" + WP_TMPDIR="$(mktemp -d)" + WP_BIN="${WP_TMPDIR}/woodpecker-cli" + + filename="woodpecker-cli_${os}_${arch}.tar.gz" + url="https://github.com/woodpecker-ci/woodpecker/releases/download/v${version}/${filename}" + + echo "Downloading woodpecker-cli v${version} (${os}/${arch})..." + curl -sL --fail -o "${WP_TMPDIR}/${filename}" "$url" || { + echo "ERROR: Failed to download woodpecker-cli from: ${url}" >&2 + return 1 + } + tar -xzf "${WP_TMPDIR}/${filename}" -C "$WP_TMPDIR" woodpecker-cli + chmod +x "$WP_BIN" +} + +# Configuration and initial values: +TOKEN="" +API_URL="" +OWNER="" +REPO="" +WORKFLOW="" +REF="main" +INPUTS=() + +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + -v|--verbose) + set -x + shift + ;; + -h|--help) + usage + exit 0 + ;; + -t|--token) + TOKEN="$2" + shift 2 + ;; + -u|--api-url) + API_URL="$2" + shift 2 + ;; + -o|--owner) + OWNER="$2" + shift 2 + ;; + -r|--repo) + REPO="$2" + shift 2 + ;; + -w|--workflow) + WORKFLOW="$2" + shift 2 + ;; + --ref) + REF="$2" + shift 2 + ;; + --input) + INPUTS+=("$2") + shift 2 + ;; + -*) + echo "ERROR: Unknown option $1" >&2 + usage + exit 1 + ;; + *) + echo "ERROR: Unexpected argument '$1'" >&2 + usage + exit 1 + ;; + esac + done +} + +parse_args "$@" + +# Validate required parameters: +errors=0 +[[ -z "$TOKEN" ]] && { echo "ERROR: --token is required." >&2; errors=$((errors+1)); } +[[ -z "$API_URL" ]] && { echo "ERROR: --api-url is required." >&2; errors=$((errors+1)); } +[[ -z "$OWNER" ]] && { echo "ERROR: --owner is required." >&2; errors=$((errors+1)); } +[[ -z "$REPO" ]] && { echo "ERROR: --repo is required." >&2; errors=$((errors+1)); } +if [[ $errors -gt 0 ]]; then + usage + exit 1 +fi + +# Strip trailing slash from API_URL: +API_URL="${API_URL%/}" + +# Download woodpecker-cli and register cleanup on exit: +download_woodpecker_cli "$WOODPECKER_CLI_VERSION" +trap cleanup EXIT + +# Build the pipeline create command. +cmd=("$WP_BIN" pipeline create "${OWNER}/${REPO}" --branch "$REF") +for var in "${INPUTS[@]}"; do + cmd+=(--var "$var") +done + +echo "Triggering pipeline for ${OWNER}/${REPO} on branch '${REF}'..." +WOODPECKER_SERVER="$API_URL" WOODPECKER_TOKEN="$TOKEN" "${cmd[@]}" + +echo "Pipeline triggered successfully." From 382f529c99cb147379afda7ef92f818c1b692f2a Mon Sep 17 00:00:00 2001 From: Danil Klimuk Date: Wed, 15 Apr 2026 12:36:59 +0200 Subject: [PATCH 03/10] .github: workflows: qubes-dom0-packagev2.yml: add qubes-component-branch Signed-off-by: Danil Klimuk --- .github/workflows/qubes-dom0-packagev2.yml | 17 +++++++++++++---- README.md | 13 +++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/qubes-dom0-packagev2.yml b/.github/workflows/qubes-dom0-packagev2.yml index d29e113..e030f85 100644 --- a/.github/workflows/qubes-dom0-packagev2.yml +++ b/.github/workflows/qubes-dom0-packagev2.yml @@ -23,6 +23,11 @@ on: Forced version of a package. required: false type: string + qubes-component-branch: + description: > + Forced repository branch to build component from + required: false + type: string jobs: build-and-package: @@ -79,6 +84,7 @@ jobs: PKG_DIR: ${{ inputs.qubes-pkg-src-dir }} PKG_REV: ${{ inputs.qubes-pkg-revision }} PKG_VER: ${{ inputs.qubes-pkg-version }} + BUILD_BRANCH: ${{ inputs.qubes-component-branch }} # Following 2 variables are used in double expansion '${${{ github.ref_type }}}', # do not change these names even though they don't follow the convention. branch: ${{ github.head_ref }} @@ -88,11 +94,14 @@ jobs: # Switch from Qubes to Docker executor sed -i "/^executor:$/,+4d; /^#executor:$/,+3s/#//" builder.yml - branch_name=${${{ github.ref_type }}} + branch_name="${BUILD_BRANCH}" if [ -z "$branch_name" ]; then - # github.head_ref is set only for pull requests, this should - # handle pushes - branch_name=$(basename "$GITHUB_REF") + branch_name=${${{ github.ref_type }}} + if [ -z "$branch_name" ]; then + # github.head_ref is set only for pull requests, this should + # handle pushes + branch_name=$(basename "$GITHUB_REF") + fi fi if [ -n "$PKG_DIR" ]; then diff --git a/README.md b/README.md index 712c4cc..0e1994c 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,13 @@ package, hence significantly reduced set of parameters. There is also no need to use `qubes-builder-docker/` in this case because builder's repository contains its own Docker image. -| Parameter | Type | Req. | Def. | Description -| --------- | ---- | ---- | ---- | ----------- -| `qubes-component` | string | Yes | - | Name of QubesOS component as recognized by its build system. -| `qubes-pkg-src-dir` | string | No | - | Relative path to directory containing Qubes OS package. -| `qubes-pkg-version` | string | No | auto | Version for RPM packages -| `qubes-pkg-revision` | string | No | `1` | Revision for RPM packages +| Parameter | Type | Req. | Def. | Description +| --------- | ---- | ---- | ---- | ----------- +| `qubes-component` | string | Yes | - | Name of QubesOS component as recognized by its build system. +| `qubes-pkg-src-dir` | string | No | - | Relative path to directory containing Qubes OS package. +| `qubes-pkg-version` | string | No | auto | Version for RPM packages +| `qubes-pkg-revision` | string | No | `1` | Revision for RPM packages +| `qubes-component-branch` | string | No | - | Forced repository branch to build component from Used by [TrenchBoot/qubes-antievilmaid][aem] and [TrenchBoot/secure-kernel-loader][skl]. The latter makes use of From 0de7bf67b7290eef7d533ba906b6c90caafe2690 Mon Sep 17 00:00:00 2001 From: Danil Klimuk Date: Wed, 15 Apr 2026 12:39:39 +0200 Subject: [PATCH 04/10] README.md: delete "Used by" for qubes-dom0-packagev2 This is because it is being used in other TrenchBoot repos as well but on other branches because of: https://github.com/TrenchBoot/grub/pull/32 https://github.com/TrenchBoot/qubes-antievilmaid/pull/15 https://github.com/TrenchBoot/xen/pull/26 IMHO there is no reason to mention every use of this workflow. Signed-off-by: Danil Klimuk --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 0e1994c..916349b 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,6 @@ builder's repository contains its own Docker image. | `qubes-pkg-revision` | string | No | `1` | Revision for RPM packages | `qubes-component-branch` | string | No | - | Forced repository branch to build component from -Used by [TrenchBoot/qubes-antievilmaid][aem] and -[TrenchBoot/secure-kernel-loader][skl]. The latter makes use of -`qubes-pkg-src-dir` as Qubes OS package is stored within the repository itself. - [qubes-builder-v2]: https://github.com/QubesOS/qubes-builderv2 [aem]: https://github.com/TrenchBoot/qubes-antievilmaid/blob/2b6b796e31789fca599986c9cfb0a3ceced5967d/.github/workflows/build.yml [skl]: https://github.com/TrenchBoot/secure-kernel-loader From ed960755b46cc723528b8d2ebb1e4f9d866c857f Mon Sep 17 00:00:00 2001 From: Danil Klimuk Date: Tue, 21 Apr 2026 20:02:17 +0200 Subject: [PATCH 05/10] Address review Signed-off-by: Danil Klimuk --- .github/workflows/rebase.yml | 2 +- README.md | 4 +- scripts/apply-qubes-patches.sh | 170 --------------------------------- scripts/rebase.sh | 162 +++++++++++++++++-------------- scripts/woodpecker-trigger.sh | 26 ++--- 5 files changed, 101 insertions(+), 263 deletions(-) delete mode 100755 scripts/apply-qubes-patches.sh diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml index 6eede9f..9c6091d 100644 --- a/.github/workflows/rebase.yml +++ b/.github/workflows/rebase.yml @@ -34,7 +34,7 @@ on: type: string commit-user-name: description: > - NAME parameter for the --upstream-branch option of the rebase.sh + NAME parameter for the --commit-user-name option of the rebase.sh script. required: true type: string diff --git a/README.md b/README.md index 916349b..3ae6cb4 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,8 @@ builder's repository contains its own Docker image. ### rebase This workflow automates rebasing a downstream repository branch on top of an -upstream branch. On success, it pushes the rebased branch. If conflicts arise -it, opens a pull request against the downstream repository to ask for +upstream branch. On success, it pushes the rebased branch. If conflicts arise, +it opens a pull request against the downstream repository to ask for resolution. | Parameter | Type | Req. | Def. | Description diff --git a/scripts/apply-qubes-patches.sh b/scripts/apply-qubes-patches.sh deleted file mode 100755 index fba1a27..0000000 --- a/scripts/apply-qubes-patches.sh +++ /dev/null @@ -1,170 +0,0 @@ -#!/bin/bash - -# SPDX-FileCopyrightText: 2026 3mdeb -# -# SPDX-License-Identifier: Apache-2.0 - -set -euo pipefail - -usage() { - cat <&2 - usage >&2 - exit 1 - ;; - *) - echo "ERROR: Unexpected argument '$1'" >&2 - usage >&2 - exit 1 - ;; - esac - done -} - -parse_args "$@" - -# Validate required parameters: -errors=0 -[[ -z "$PATCHES_REPO" ]] && { echo "ERROR: --patches-repo is required." >&2; errors=$((errors+1)); } -[[ -z "$DOWNSTREAM_REPO" ]] && { echo "ERROR: --downstream-repo is required." >&2; errors=$((errors+1)); } -[[ -z "$UPSTREAM_URL" ]] && { echo "ERROR: --upstream-url is required." >&2; errors=$((errors+1)); } -[[ -z "$UPSTREAM_TAG_PREFIX" ]] && { echo "ERROR: --upstream-tag-prefix is required." >&2; errors=$((errors+1)); } -[[ -z "$RESULT_BRANCH" ]] && { echo "ERROR: --result-branch is required." >&2; errors=$((errors+1)); } -[[ -z "$TOKEN" ]] && { echo "ERROR: --token is required." >&2; errors=$((errors+1)); } -if [[ $errors -gt 0 ]]; then - usage >&2 - exit 1 -fi - -# Clone the patches repository: -echo "Cloning patches repository ${PATCHES_REPO}..." -git clone "https://github.com/${PATCHES_REPO}.git" patches-repo - -# Read the upstream version from the patches repository's version file: -VERSION="$(tr -d '[:space:]' < patches-repo/version)" -UPSTREAM_TAG="${UPSTREAM_TAG_PREFIX}${VERSION}" -echo "Upstream version: ${VERSION}, tag: ${UPSTREAM_TAG}" - -# Clone the downstream repository using the provided token: -echo "Cloning downstream repository ${DOWNSTREAM_REPO}..." -git clone "https://x-access-token:${TOKEN}@github.com/${DOWNSTREAM_REPO}.git" downstream-repo - -cd downstream-repo - -# Add the upstream remote and fetch only the required tag: -echo "Fetching upstream tag ${UPSTREAM_TAG} from ${UPSTREAM_URL}..." -git remote add upstream "$UPSTREAM_URL" -git fetch upstream "refs/tags/${UPSTREAM_TAG}:refs/tags/${UPSTREAM_TAG}" - -# Configure the git identity used for the commits created by git am: -git config user.name 'github-actions[bot]' -git config user.email 'github-actions[bot]@users.noreply.github.com' - -# Create the result branch at the upstream tag and apply the patches: -echo "Creating branch '${RESULT_BRANCH}' at tag '${UPSTREAM_TAG}'..." -git checkout -b "$RESULT_BRANCH" "$UPSTREAM_TAG" - -echo "Applying patches from ${PATCHES_REPO}..." -git am ../patches-repo/*.patch - -# Force-push the result branch to the downstream repository so that re-runs -# are idempotent: -echo "Pushing '${RESULT_BRANCH}' to ${DOWNSTREAM_REPO}..." -git push --force origin "$RESULT_BRANCH" - -echo "Done. Branch '${RESULT_BRANCH}' pushed to ${DOWNSTREAM_REPO}." diff --git a/scripts/rebase.sh b/scripts/rebase.sh index 12a161c..3775590 100755 --- a/scripts/rebase.sh +++ b/scripts/rebase.sh @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +trap error_handling EXIT + set -euo pipefail # Help @@ -126,14 +128,14 @@ EOF # 0: Success. # 1: Some issue. push_branch_remote() { - local token="${1:-}" - local branch="${2:-}" - local remote="${3:-}" + local token="$1" + local branch="$2" + local remote="$3" # The remote URL must contain the token for the ref to be modified on the # remote via personal access token authentication: - git remote get-url "$remote" 2> /dev/null | grep -F "$token" &> /dev/null || return 1 - git push "$remote" "$branch" &> /dev/null || return 1 + git remote get-url "$remote" 2> "$TMP_LOG_FILE" | grep -F "$token" &> "$TMP_LOG_FILE" || return 1 + git push "$remote" "$branch" &> "$TMP_LOG_FILE" || return 1 return 0 } @@ -144,9 +146,9 @@ push_branch_remote() { # 2: The function tried to delete a branch that was not created by this script # and probably belongs to somebody else. delete_branch_remote() { - local token="${1:-}" - local branch="${2:-}" - local remote="${3:-}" + local token="$1" + local branch="$2" + local remote="$3" local temp="" local commit="" @@ -154,11 +156,11 @@ delete_branch_remote() { # script and not some other branch: # 1. The branch must match the pattern for branches with conflicts that are # created by this script: - echo "$branch" | grep -E '.*-[a-z0-9]{40}-conflict' &> /dev/null || return 2 + echo "$branch" | grep -E '.*-[a-z0-9]{40}-conflict' &> "$TMP_LOG_FILE" || return 2 # 2. The branch name must contain a hash of existing commit temp="${branch%-conflict}" commit="${temp##*-}" - git show "$commit" &> /dev/null || return 2 + git show "$commit" &> "$TMP_LOG_FILE" || return 2 # The checks above are reasonable but not sufficient, as there is a # probability that a branch that will match the pattern will be created by a # user. At git level we do not have access to the information on who created @@ -172,8 +174,8 @@ delete_branch_remote() { # The remote URL must contain the token for the ref to be modified on the # remote via personal access token authentication: - git remote get-url "$remote" 2> /dev/null | grep -F "$token" &> /dev/null || return 1 - git push "$remote" --delete "$branch" &> /dev/null || return 1 + git remote get-url "$remote" 2> "$TMP_LOG_FILE" | grep -F "$token" &> "$TMP_LOG_FILE" || return 1 + git push "$remote" --delete "$branch" &> "$TMP_LOG_FILE" || return 1 return 0 } @@ -183,12 +185,12 @@ delete_branch_remote() { # 1: Some issue. # 2: PR was not created. create_pr_remote() { - local token="${1:-}" - local repo_url="${2:-}" - local head_branch="${3:-}" - local base_branch="${4:-}" - local pr_title="${5:-}" - local pr_body="${6:-}" + local token="$1" + local repo_url="$2" + local head_branch="$3" + local base_branch="$4" + local pr_title="$5" + local pr_body="$6" local payload repo_path owner repo_name response http_code pr_url body @@ -261,6 +263,17 @@ print(json.dumps({ return 0 } +error_handling() { + exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "ERROR: $BASH_COMMAND failed!" >&2 + echo -e "The logs from the last executed command:\n" >&2 + cat "$TMP_LOG_FILE" >&2 + fi + + rm -f "$TMP_LOG_FILE" +} + parse_args() { while [[ $# -gt 0 ]]; do case $1 in @@ -310,16 +323,16 @@ parse_args() { # 0: Success. # 1: No HTTPS protocol prefix found in the remote repository URL. build_url_with_token() { - local url="${1:-}" - local token="${2:-}" + local url="$1" + local token="$2" local repo_path # The limit on HTTPS only is because of the personal access token that is # used here and works only over HTTPS: - echo "$url" | grep 'https://' &> /dev/null || return 1 + echo "$url" | grep 'https://' &> "$TMP_LOG_FILE" || return 1 repo_path="$(echo "$url" | sed -E 's|.*github\.com[:/]||; s|\.git$||')" - echo "https://$token@github.com/$repo_path.git" 2> /dev/null + echo "https://$token@github.com/$repo_path.git" 2> "$TMP_LOG_FILE" return 0 } @@ -328,11 +341,11 @@ build_url_with_token() { # 0: Rebase is needed. # 1: Rebase is not needed. check_for_rebase() { - local head_ref="${1:-}" - local newbase="${2:-}" + local head_ref="$1" + local newbase="$2" local result - result="$(git log "$head_ref".."$newbase" --oneline 2> /dev/null)" + result="$(git log "$head_ref".."$newbase" --oneline 2> "$TMP_LOG_FILE")" if [[ -z "$result" ]]; then return 1 @@ -346,10 +359,10 @@ check_for_rebase() { # 0: Does exist. # 1: Does not exist. check_for_pr() { - local token="${1:-}" - local repo_url="${2:-}" - local branch1="${3:-}" - local branch2="${4:-}" + local token="$1" + local repo_url="$2" + local branch1="$3" + local branch2="$4" local result=1 local response repo_path owner repo_name @@ -371,7 +384,7 @@ check_for_pr() { result="$(python3 -c "import sys, json; print(len(json.load(sys.stdin)))" <<< "$response")" fi - if [ $result -eq 0 ]; then + if [ "$result" -eq 0 ]; then return 1 fi @@ -384,16 +397,16 @@ check_for_pr() { # 1: The conflict is not resolved or the commit is not present. Or in some other # undefined state check_if_resolved() { - local head_ref=${1:-} - local base=${2:-} - local newbase=${3:-} + local head_ref="$1" + local base="$2" + local newbase="$3" local commits1 commits1_num commits2 commits2_num commits1="$(git log "$newbase".."$head_ref" --oneline)" commits2="$(git log "$newbase".."$base" --oneline)" - commits1_num=$(printf '%s' "$commits1" | wc -l) - commits2_num=$(printf '%s' "$commits2" | wc -l) + commits1_num=$(printf '%s' "$commits1" | grep -c '.' ) + commits2_num=$(printf '%s' "$commits2" | grep -c '.' ) if [[ $commits1_num -eq $commits2_num ]]; then return 0 @@ -413,6 +426,7 @@ COMMIT_USER_NAME="github-actions[bot]" COMMIT_USER_EMAIL="github-actions[bot]@users.noreply.github.com" CICD_TRIGGER_RESUME="" REBASE_HEAD_FILE=".git/REBASE_HEAD" +TMP_LOG_FILE="$(mktemp)" POSITIONAL_ARGS=() @@ -443,20 +457,20 @@ else REPO_DIR="$FIRST_REPO" fi -SUCCESSULL_REBASE_PR_TITLE="Automatic rebase of branch $FIRST_REPO_BRANCH completed successfuly" -SUCCESSULL_REBASE_MESSAGE=" +SUCCESSFUL_REBASE_PR_TITLE="Automatic rebase of branch $FIRST_REPO_BRANCH completed successfuly" +SUCCESSFUL_REBASE_MESSAGE=" Summary: * Rebased branch $FIRST_REPO_BRANCH from repository $FIRST_REPO." if [[ -z "$LOCAL" ]]; then - SUCCESSULL_REBASE_MESSAGE+=" + SUCCESSFUL_REBASE_MESSAGE+=" * New base: $SECOND_REPO_BRANCH from repository $SECOND_REPO." else - SUCCESSULL_REBASE_MESSAGE+=" + SUCCESSFUL_REBASE_MESSAGE+=" * New base: $SECOND_REPO_BRANCH from repository $FIRST_REPO." fi -SUCCESSULL_REBASE_MESSAGE+=" +SUCCESSFUL_REBASE_MESSAGE+=" Please, manage the rebased branch by either merging $FIRST_REPO_BRANCH-rebased into $FIRST_REPO_BRANCH, force pushing branch $FIRST_REPO_BRANCH to include @@ -476,39 +490,39 @@ if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then repo_path="$(build_url_with_token "$FIRST_REPO" "$TOKEN")" echo "Cloning the first repository: $FIRST_REPO..." - git clone "$repo_path" "$REPO_DIR" &> /dev/null - cd "$REPO_DIR" &> /dev/null - git fetch --all &> /dev/null - cd - &> /dev/null + git clone "$repo_path" "$REPO_DIR" &> "$TMP_LOG_FILE" + cd "$REPO_DIR" &> "$TMP_LOG_FILE" + git fetch --all &> "$TMP_LOG_FILE" + cd - &> "$TMP_LOG_FILE" unset repo_path elif [[ "$LOCAL" != "true" ]]; then echo "Cloning the first repository: $FIRST_REPO..." - git clone "$FIRST_REPO" "$REPO_DIR" &> /dev/null + git clone "$FIRST_REPO" "$REPO_DIR" &> "$TMP_LOG_FILE" fi echo "Checking out branch '$FIRST_REPO_BRANCH'..." -cd "$REPO_DIR" &> /dev/null -git checkout "$FIRST_REPO_BRANCH" &> /dev/null +cd "$REPO_DIR" &> "$TMP_LOG_FILE" +git checkout "$FIRST_REPO_BRANCH" &> "$TMP_LOG_FILE" echo "Setting user name to $COMMIT_USER_NAME for commits..." -git config user.name "$COMMIT_USER_NAME" &> /dev/null +git config user.name "$COMMIT_USER_NAME" &> "$TMP_LOG_FILE" echo "Setting user email to $COMMIT_USER_EMAIL for commits..." -git config user.email "$COMMIT_USER_EMAIL" &> /dev/null +git config user.email "$COMMIT_USER_EMAIL" &> "$TMP_LOG_FILE" # Add second repo as a remote: if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then echo "Adding second repo as a remote $SECOND_REPO..." - git remote add "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO" &> /dev/null + git remote add "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO" &> "$TMP_LOG_FILE" unset repo_path echo "Fetching from the second repo branch '$SECOND_REPO_BRANCH'..." - git fetch "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO_BRANCH" &> /dev/null + git fetch "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO_BRANCH" &> "$TMP_LOG_FILE" elif [[ "$LOCAL" != "true" ]]; then echo "Adding second repo as a remote $SECOND_REPO..." - git remote add "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO" &> /dev/null + git remote add "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO" &> "$TMP_LOG_FILE" echo "Fetching from the second repo '$SECOND_REPO_BRANCH'..." - git fetch "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO_BRANCH" &> /dev/null + git fetch "$SECOND_REPO_REMOTE_NAME" "$SECOND_REPO_BRANCH" &> "$TMP_LOG_FILE" fi ################################################################################ @@ -517,12 +531,12 @@ fi # Check if there is a FIRST_REPO_BRANCH-rebased branch. If yes - do not start a # new rebase, as the last successful rebase was not managed properly. if [[ "$LOCAL" != "true" ]]; then - BRANCH_TEMP=$(git branch -r --no-column --list "$FIRST_REPO_REMOTE_NAME"/"$FIRST_REPO_BRANCH"-rebased 2> /dev/null) + BRANCH_TEMP=$(git branch -r --no-column --list "$FIRST_REPO_REMOTE_NAME"/"$FIRST_REPO_BRANCH"-rebased 2> "$TMP_LOG_FILE") else - BRANCH_TEMP=$(git branch --no-column --list "$FIRST_REPO_BRANCH"-rebased 2> /dev/null) + BRANCH_TEMP=$(git branch --no-column --list "$FIRST_REPO_BRANCH"-rebased 2> "$TMP_LOG_FILE") fi -if echo "$BRANCH_TEMP" | grep rebased &> /dev/null; then +if echo "$BRANCH_TEMP" | grep rebased &> "$TMP_LOG_FILE"; then echo "The last successful rebase of the branch $FIRST_REPO_BRANCH is still present in the repository history. Please merge or delete it and restart the automatic rebase." @@ -534,9 +548,9 @@ unset BRANCH_TEMP # consequently triggered after a manual conflict resolution rebase attempt. # Search for a previous branch with a conflict: if [[ "$LOCAL" != "true" ]]; then - BRANCH_TEMP=$(git branch -r --no-column --list "$FIRST_REPO_REMOTE_NAME"/"$FIRST_REPO_BRANCH"-*-conflict 2> /dev/null) + BRANCH_TEMP=$(git branch -r --no-column --list "$FIRST_REPO_REMOTE_NAME"/"$FIRST_REPO_BRANCH"-*-conflict 2> "$TMP_LOG_FILE") else - BRANCH_TEMP=$(git branch --no-column --list "$FIRST_REPO_BRANCH"-*-conflict 2> /dev/null) + BRANCH_TEMP=$(git branch --no-column --list "$FIRST_REPO_BRANCH"-*-conflict 2> "$TMP_LOG_FILE") fi while IFS= read -r line; do @@ -573,19 +587,19 @@ if [[ -z "$COMMIT" && -z "$BRANCH" ]]; then echo "Rebasing '$FIRST_REPO_BRANCH' onto '$SECOND_REPO_REF'..." - if git rebase "$SECOND_REPO_REF" &> /dev/null; then + if git rebase "$SECOND_REPO_REF" &> "$TMP_LOG_FILE"; then echo "Rebase completed successfuly. No conflicts." # Do not push to the same branch on the remote repository to avoid # force pushes: - git checkout -b "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" &> /dev/null + git checkout -b "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" &> "$TMP_LOG_FILE" if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then push_branch_remote "$TOKEN" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_REMOTE_NAME" if ! check_for_pr "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" ; then - create_pr_remote "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" "$SUCCESSULL_REBASE_PR_TITLE" "$SUCCESSULL_REBASE_MESSAGE" || exit 1 + create_pr_remote "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" "$SUCCESSFUL_REBASE_PR_TITLE" "$SUCCESSFUL_REBASE_MESSAGE" fi else - echo "$SUCCESSULL_REBASE_MESSAGE" + echo "$SUCCESSFUL_REBASE_MESSAGE" fi exit 0 fi @@ -597,11 +611,11 @@ elif [[ -n "$COMMIT" && -n "$BRANCH" ]]; then echo "Continuing rebase '$FIRST_REPO_BRANCH' onto '$BRANCH' using commit $COMMIT as a base..." - if git rebase --onto "$BRANCH" "$COMMIT" "$FIRST_REPO_BRANCH" &> /dev/null; then + if git rebase --onto "$BRANCH" "$COMMIT" "$FIRST_REPO_BRANCH" &> "$TMP_LOG_FILE"; then # Delete the temporary conflict branch so there is no leftovers after a # success: - git branch --delete "$BRANCH" &> /dev/null - git checkout -b "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" &> /dev/null + git branch --delete "$BRANCH" &> "$TMP_LOG_FILE" + git checkout -b "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" &> "$TMP_LOG_FILE" if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then # Delete the temporary conflict branch on remote so there is no @@ -612,10 +626,10 @@ elif [[ -n "$COMMIT" && -n "$BRANCH" ]]; then # force pushes: push_branch_remote "$TOKEN" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_REMOTE_NAME" if ! check_for_pr "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" ; then - create_pr_remote "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" "$SUCCESSULL_REBASE_PR_TITLE" "$SUCCESSULL_REBASE_MESSAGE" || exit 1 + create_pr_remote "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" "$SUCCESSFUL_REBASE_PR_TITLE" "$SUCCESSFUL_REBASE_MESSAGE" fi else - echo "$SUCCESSULL_REBASE_MESSAGE" + echo "$SUCCESSFUL_REBASE_MESSAGE" fi echo "Rebase completed successfuly. No new conflicts." @@ -635,11 +649,11 @@ echo "Conflict detected. Inspecting rebase state..." if [[ ! -f "$REBASE_HEAD_FILE" ]]; then echo "ERROR: Expected .git/REBASE_HEAD not found. Cannot determine conflicting commit." >&2 - git rebase --abort &> /dev/null + git rebase --abort &> "$TMP_LOG_FILE" exit 3 fi -CONFLICT_COMMIT=$(cat "$REBASE_HEAD_FILE" 2> /dev/null) +CONFLICT_COMMIT=$(cat "$REBASE_HEAD_FILE" 2> "$TMP_LOG_FILE") # Build the conflict branch name: --conflict CONFLICT_BRANCH="${FIRST_REPO_BRANCH}-${CONFLICT_COMMIT}-conflict" @@ -653,20 +667,20 @@ CONFLICT_BRANCH="${FIRST_REPO_BRANCH}-${CONFLICT_COMMIT}-conflict" # pushed to the branch after resolution by the developer. Hence, no need to # create a branch. if [[ "$BRANCH" != "$CONFLICT_BRANCH" ]]; then - git branch "$CONFLICT_BRANCH" &> /dev/null + git branch "$CONFLICT_BRANCH" &> "$TMP_LOG_FILE" if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then - push_branch_remote "$TOKEN" "$CONFLICT_BRANCH" "$FIRST_REPO_REMOTE_NAME" || exit 1 + push_branch_remote "$TOKEN" "$CONFLICT_BRANCH" "$FIRST_REPO_REMOTE_NAME" fi if [[ -n "$BRANCH" ]]; then - git branch --delete "$BRANCH" &> /dev/null + git branch --delete "$BRANCH" &> "$TMP_LOG_FILE" if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then delete_branch_remote "$TOKEN" "$BRANCH" "$FIRST_REPO_REMOTE_NAME" fi fi fi -git rebase --abort &> /dev/null +git rebase --abort &> "$TMP_LOG_FILE" ################################################################################ # Opening a PR/communicating via CLI with instructions on how to proceed: @@ -767,7 +781,7 @@ if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then pr_title="Automatic rebase of branch '$FIRST_REPO_BRANCH' met a conflict." if ! check_for_pr "$TOKEN" "$FIRST_REPO" "$CONFLICT_BRANCH" "$FIRST_REPO_BRANCH" ; then - create_pr_remote "$TOKEN" "$FIRST_REPO" "$CONFLICT_BRANCH" "$FIRST_REPO_BRANCH" "$pr_title" "$pr_body" || exit 1 + create_pr_remote "$TOKEN" "$FIRST_REPO" "$CONFLICT_BRANCH" "$FIRST_REPO_BRANCH" "$pr_title" "$pr_body" fi else echo "$message" diff --git a/scripts/woodpecker-trigger.sh b/scripts/woodpecker-trigger.sh index e18b073..ff84315 100755 --- a/scripts/woodpecker-trigger.sh +++ b/scripts/woodpecker-trigger.sh @@ -4,16 +4,9 @@ # # SPDX-License-Identifier: Apache-2.0 +trap cleanup EXIT set -euo pipefail -# Pinned woodpecker-cli version. Override with the WOODPECKER_CLI_VERSION -# environment variable. -# Check https://github.com/woodpecker-ci/woodpecker/releases for available -# versions. -WOODPECKER_CLI_VERSION="${WOODPECKER_CLI_VERSION:-3.13.0}" -WP_BIN="" -WP_TMPDIR="" - usage() { cat < Date: Tue, 21 Apr 2026 21:45:37 +0200 Subject: [PATCH 06/10] scripts: rebase.sh: add empty commits handling Signed-off-by: Danil Klimuk --- scripts/rebase.sh | 53 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/scripts/rebase.sh b/scripts/rebase.sh index 3775590..38d656e 100755 --- a/scripts/rebase.sh +++ b/scripts/rebase.sh @@ -415,6 +415,24 @@ check_if_resolved() { return 1 } +# This function checks for empty commits between two refs. There is no return +# values. Instead it prints a list of empty commits to the STDOUT. +check_for_empty_commits() { + local base="$1" + local head_ref="$2" + local commits="" + local temp="" + + temp="$(git log "$base"^.."$head_ref" --format="%H %s" 2> "$TMP_LOG_FILE")" + while read sha msg; do + [ -z "$(git diff-tree --no-commit-id -r "$sha" 2> "$TMP_LOG_FILE")" ] \ + && commits+=" +$sha $msg" + done <<< "$temp" + + echo "$commits" +} + # Configuration and initial values: declare -a BRANCHES declare BRANCH_TEMP @@ -587,12 +605,27 @@ if [[ -z "$COMMIT" && -z "$BRANCH" ]]; then echo "Rebasing '$FIRST_REPO_BRANCH' onto '$SECOND_REPO_REF'..." - if git rebase "$SECOND_REPO_REF" &> "$TMP_LOG_FILE"; then + # The check_if_resolved() function checks whether the conflict has been + # resolved by comparing the number of commits on the conflict branch, the + # default behavior of the git rebase is to drop empty commits that are ther + # result of rebase operation. This could mess up the check_if_resolved(). + # Hence, we better keep the empty commits on the branch but inform the + # developer about them. + if git rebase --empty=keep "$SECOND_REPO_REF" &> "$TMP_LOG_FILE"; then echo "Rebase completed successfuly. No conflicts." # Do not push to the same branch on the remote repository to avoid # force pushes: git checkout -b "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" &> "$TMP_LOG_FILE" + empty_commits="$(check_for_empty_commits "$SECOND_REPO_REF" "$FIRST_REPO_BRANCH-rebased")" + + if [ -n "$empty_commits" ]; then + SUCCESSFUL_REBASE_MESSAGE+=" +$empty_commits + +You might want to drop them." + fi + if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then push_branch_remote "$TOKEN" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_REMOTE_NAME" if ! check_for_pr "$TOKEN" "$FIRST_REPO" "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" ; then @@ -610,12 +643,26 @@ elif [[ -n "$COMMIT" && -n "$BRANCH" ]]; then fi echo "Continuing rebase '$FIRST_REPO_BRANCH' onto '$BRANCH' using commit $COMMIT as a base..." - - if git rebase --onto "$BRANCH" "$COMMIT" "$FIRST_REPO_BRANCH" &> "$TMP_LOG_FILE"; then + + # The check_if_resolved() function checks whether the conflict has been + # resolved by comparing the number of commits on the conflict branch, the + # default behavior of the git rebase is to drop empty commits that are ther + # result of rebase operation. This could mess up the check_if_resolved(). + # Hence, we better keep the empty commits on the branch but inform the + # developer about them. + if git rebase --empty=keep --onto "$BRANCH" "$COMMIT" "$FIRST_REPO_BRANCH" &> "$TMP_LOG_FILE"; then # Delete the temporary conflict branch so there is no leftovers after a # success: git branch --delete "$BRANCH" &> "$TMP_LOG_FILE" git checkout -b "$FIRST_REPO_BRANCH-rebased" "$FIRST_REPO_BRANCH" &> "$TMP_LOG_FILE" + empty_commits="$(check_for_empty_commits "$SECOND_REPO_REF" "$FIRST_REPO_BRANCH-rebased")" + + if [ -n "$empty_commits" ]; then + SUCCESSFUL_REBASE_MESSAGE+=" +$empty_commits + +You might want to drop them." + fi if [[ "$LOCAL" != "true" && -n "$TOKEN" ]]; then # Delete the temporary conflict branch on remote so there is no From 2d9d77c6892cb5164471bba779a8f68373dd02ca Mon Sep 17 00:00:00 2001 From: Danil Klimuk Date: Wed, 22 Apr 2026 11:54:20 +0200 Subject: [PATCH 07/10] scripts: rebase.sh: add a note about empty commits Signed-off-by: Danil Klimuk --- scripts/rebase.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/rebase.sh b/scripts/rebase.sh index 38d656e..422ac09 100755 --- a/scripts/rebase.sh +++ b/scripts/rebase.sh @@ -772,7 +772,10 @@ message+=" \`\`\` 5. Solve the conflict and apply the commit after solving the conflict on top of - the conflict branch: + the conflict branch. Important: if the conflict resolution resulted in an + empty commit or you have decided not to resolve the conflict but to drop the + commit - you must still add one commit to the $CONFLICT_BRANCH, even if it + is an empty commit. Otherwise the automated rebase will not continue. \`\`\` git add . From 08b43ea3651f304adaf8245bc67fa76955d44441 Mon Sep 17 00:00:00 2001 From: Danil Klimuk Date: Thu, 23 Apr 2026 11:36:54 +0200 Subject: [PATCH 08/10] .github: workflows: update actions/checkout to v6 Signed-off-by: Danil Klimuk --- .github/workflows/qubes-dom0-package.yml | 4 ++-- .github/workflows/qubes-dom0-packagev2.yml | 2 +- .github/workflows/trigger-woodpecker-pipeline.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/qubes-dom0-package.yml b/.github/workflows/qubes-dom0-package.yml index dee3f8b..00d8431 100644 --- a/.github/workflows/qubes-dom0-package.yml +++ b/.github/workflows/qubes-dom0-package.yml @@ -42,11 +42,11 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 100 # need history for `git format-patch` - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: repository: TrenchBoot/.github path: shared diff --git a/.github/workflows/qubes-dom0-packagev2.yml b/.github/workflows/qubes-dom0-packagev2.yml index e030f85..7243bd4 100644 --- a/.github/workflows/qubes-dom0-packagev2.yml +++ b/.github/workflows/qubes-dom0-packagev2.yml @@ -45,7 +45,7 @@ jobs: createrepo-c devscripts python3-docker reprepro \ python3-pathspec mktorrent python3-lxml python3-dateutil - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: repository: QubesOS/qubes-builderv2 ref: 80dd898cc0472dd99f161f1d1c7c44da64de93f2 diff --git a/.github/workflows/trigger-woodpecker-pipeline.yml b/.github/workflows/trigger-woodpecker-pipeline.yml index dc387cc..b29ad1f 100644 --- a/.github/workflows/trigger-woodpecker-pipeline.yml +++ b/.github/workflows/trigger-woodpecker-pipeline.yml @@ -50,7 +50,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: repository: TrenchBoot/.github path: shared From e51d845a11095ed012713d56a9387829524ee989 Mon Sep 17 00:00:00 2001 From: Danil Klimuk Date: Thu, 23 Apr 2026 12:02:00 +0200 Subject: [PATCH 09/10] scripts: rebase.sh: Address review Signed-off-by: Danil Klimuk --- .github/workflows/rebase.yml | 2 +- .../workflows/trigger-woodpecker-pipeline.yml | 2 +- scripts/rebase.sh | 30 +++++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml index 9c6091d..c31a768 100644 --- a/.github/workflows/rebase.yml +++ b/.github/workflows/rebase.yml @@ -63,7 +63,7 @@ jobs: with: repository: TrenchBoot/.github path: shared - ref: master + ref: ${{ job.workflow_sha }} - name: Run script for rebasing env: FIRST_REMOTE_TOKEN: ${{ secrets.first-remote-token }} diff --git a/.github/workflows/trigger-woodpecker-pipeline.yml b/.github/workflows/trigger-woodpecker-pipeline.yml index b29ad1f..e693a92 100644 --- a/.github/workflows/trigger-woodpecker-pipeline.yml +++ b/.github/workflows/trigger-woodpecker-pipeline.yml @@ -54,7 +54,7 @@ jobs: with: repository: TrenchBoot/.github path: shared - ref: master + ref: ${{ job.workflow_sha }} - name: Trigger Woodpecker CI/CD pipeline env: WOODPECKER_TOKEN: ${{ secrets.woodpecker-token }} diff --git a/scripts/rebase.sh b/scripts/rebase.sh index 422ac09..a2d9d38 100755 --- a/scripts/rebase.sh +++ b/scripts/rebase.sh @@ -95,12 +95,13 @@ Options: developer via the MESSAGE how to relaunch automatic rebase after resolving the conflict. -v, --verbose Print a lot of debug information. + Note: token value will be visible in output. Conflict branch naming: -<40-char-hash-of-conflicting-commit>-conflict Exit codes: - 0 Rebase completed successfuly + 0 Rebase completed successfully 1 Some other issue encountered 2 Conflict encountered (conflict branch created) 3 Script logic failure @@ -268,10 +269,11 @@ error_handling() { if [ $exit_code -ne 0 ]; then echo "ERROR: $BASH_COMMAND failed!" >&2 echo -e "The logs from the last executed command:\n" >&2 - cat "$TMP_LOG_FILE" >&2 + [ -f "${TMP_LOG_FILE:-}" ] && cat "$TMP_LOG_FILE" >&2 fi - rm -f "$TMP_LOG_FILE" + rm -f "${TMP_LOG_FILE:-}" + rm -rf "${WORK_DIR:-}" } parse_args() { @@ -423,8 +425,8 @@ check_for_empty_commits() { local commits="" local temp="" - temp="$(git log "$base"^.."$head_ref" --format="%H %s" 2> "$TMP_LOG_FILE")" - while read sha msg; do + temp="$(git log "$base".."$head_ref" --format="%H %s" 2> "$TMP_LOG_FILE")" + while IFS= read -r sha msg; do [ -z "$(git diff-tree --no-commit-id -r "$sha" 2> "$TMP_LOG_FILE")" ] \ && commits+=" $sha $msg" @@ -451,6 +453,10 @@ POSITIONAL_ARGS=() parse_args "$@" set -- "${POSITIONAL_ARGS[@]}" +if [[ "${#POSITIONAL_ARGS[@]}" -lt "2" ]]; then + usage + exit 1 +fi FIRST_REPO="$1" FIRST_REPO_BRANCH="$2" FIRST_REPO_REMOTE_NAME="origin" @@ -475,7 +481,7 @@ else REPO_DIR="$FIRST_REPO" fi -SUCCESSFUL_REBASE_PR_TITLE="Automatic rebase of branch $FIRST_REPO_BRANCH completed successfuly" +SUCCESSFUL_REBASE_PR_TITLE="Automatic rebase of branch $FIRST_REPO_BRANCH completed successfully" SUCCESSFUL_REBASE_MESSAGE=" Summary: * Rebased branch $FIRST_REPO_BRANCH from repository $FIRST_REPO." @@ -607,12 +613,12 @@ if [[ -z "$COMMIT" && -z "$BRANCH" ]]; then # The check_if_resolved() function checks whether the conflict has been # resolved by comparing the number of commits on the conflict branch, the - # default behavior of the git rebase is to drop empty commits that are ther + # default behavior of the git rebase is to drop empty commits that are there # result of rebase operation. This could mess up the check_if_resolved(). # Hence, we better keep the empty commits on the branch but inform the # developer about them. if git rebase --empty=keep "$SECOND_REPO_REF" &> "$TMP_LOG_FILE"; then - echo "Rebase completed successfuly. No conflicts." + echo "Rebase completed successfully. No conflicts." # Do not push to the same branch on the remote repository to avoid # force pushes: @@ -646,7 +652,7 @@ elif [[ -n "$COMMIT" && -n "$BRANCH" ]]; then # The check_if_resolved() function checks whether the conflict has been # resolved by comparing the number of commits on the conflict branch, the - # default behavior of the git rebase is to drop empty commits that are ther + # default behavior of the git rebase is to drop empty commits that are there # result of rebase operation. This could mess up the check_if_resolved(). # Hence, we better keep the empty commits on the branch but inform the # developer about them. @@ -679,7 +685,7 @@ You might want to drop them." echo "$SUCCESSFUL_REBASE_MESSAGE" fi - echo "Rebase completed successfuly. No new conflicts." + echo "Rebase completed successfully. No new conflicts." exit 0 fi else @@ -706,7 +712,7 @@ CONFLICT_COMMIT=$(cat "$REBASE_HEAD_FILE" 2> "$TMP_LOG_FILE") CONFLICT_BRANCH="${FIRST_REPO_BRANCH}-${CONFLICT_COMMIT}-conflict" -# Create a branch at HEAD (last successfuly rebased commit) before aborting to +# Create a branch at HEAD (last successfully rebased commit) before aborting to # save the current state of the rebase. Delete the previous temporary conflict # branch to prevent the situation that cause this script to return code 4 (see # the usage). If the branch that was used during the rebase has the same name as @@ -744,7 +750,7 @@ if [[ "$LOCAL" != "true" ]]; then * Second repo branch : $SECOND_REPO_BRANCH" fi message+=" -* Branch with the successfuly rebased commits : $CONFLICT_BRANCH +* Branch with the successfully rebased commits : $CONFLICT_BRANCH * The commit that introduced the conflict : $CONFLICT_COMMIT Before relaunching the automatic rebase, please do the following to solve the From b70dcd65c84fbb70b4b539644481e83a350d2744 Mon Sep 17 00:00:00 2001 From: Danil Klimuk Date: Mon, 27 Apr 2026 10:30:16 +0200 Subject: [PATCH 10/10] .github: workflows: rebase.yml: add rebase-exit-code output, the return code 5 should be treated as a success Signed-off-by: Danil Klimuk --- .github/workflows/rebase.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml index c31a768..8b72a8b 100644 --- a/.github/workflows/rebase.yml +++ b/.github/workflows/rebase.yml @@ -50,14 +50,22 @@ on: rebase.sh script. required: true type: string + outputs: + rebase-exit-code: + description: > + Exit code returned by the rebase.sh script. See the script's --help + output for the meaning of each code. + value: ${{ jobs.rebase-attempt.outputs.rebase-exit-code }} jobs: - build-and-package: + rebase-attempt: runs-on: ubuntu-latest name: Try rebasing on updated upstream, report in case of conflicts permissions: # For creation/deletion/pushing to branches and creating PRs contents: write + outputs: + rebase-exit-code: ${{ steps.rebase.outputs.exit-code }} steps: - uses: actions/checkout@v6 with: @@ -65,6 +73,7 @@ jobs: path: shared ref: ${{ job.workflow_sha }} - name: Run script for rebasing + id: rebase env: FIRST_REMOTE_TOKEN: ${{ secrets.first-remote-token }} DOWNSTREAM_REPO: ${{ inputs.downstream-repo }} @@ -75,6 +84,7 @@ jobs: EMAIL: ${{ inputs.commit-user-email }} MESSAGE: ${{ inputs.cicd-trigger-resume }} run: | + set +e shared/scripts/rebase.sh --first-remote-token "$FIRST_REMOTE_TOKEN" \ --commit-user-name "$NAME" \ --commit-user-email "$EMAIL" \ @@ -83,3 +93,12 @@ jobs: "$DOWNSTREAM_BRANCH" \ "$UPSTREAM_REPO" \ "$UPSTREAM_BRANCH" + rc=$? + echo "exit-code=${rc}" >> "$GITHUB_OUTPUT" + # The "No rebase needed" return code should be considered a success + # here, as we do not want to show that a job has failed in that case + # to avoid drawing attention of maintainers. + if [ "$rc" -eq "5" ]; then + exit "0" + fi + exit "${rc}"