--- /dev/null
+#!/bin/bash
+# Run GitLab CI jobs locally via gitlab-ci-local, working around its quirks.
+#
+# Usage:
+# devel/ci-local.sh --needs 'fedora/test: [no-fips,no-pqc,no-ktls]'
+#
+# More useful gitlab-ci-local options: --privileged
+#
+# Dependencies: gitlab-ci-local, yq, podman or docker
+
+set -Eeuo pipefail; shopt -s inherit_errexit
+
+CI_REGISTRY="${CI_REGISTRY:-registry.gitlab.com}"
+CONTAINER_EXECUTABLE="${CONTAINER_EXECUTABLE:-podman}"
+SKIP_COMMIT_CHECK="${SKIP_COMMIT_CHECK:-true}"
+CLONE_SUBMODULES="${CLONE_SUBMODULES:-true}"
+
+[[ -e .gitlab-ci.yml ]] || { echo "error: run from the git root" >&2; exit 1; }
+
+cleanup=()
+on_exit() {
+ # sync .gitlab-ci-local back to the worktree for caching
+ if [[ -n "${srcdir:-}" ]] && [[ -d .gitlab-ci-local ]]; then
+ echo "copying .gitlab-ci-local back to $srcdir" >&2
+ rsync -a .gitlab-ci-local "$srcdir/"
+ fi
+ [[ -z "${cleanup[*]}" ]] || rm -rf "${cleanup[@]}"
+}
+trap on_exit EXIT
+
+# Quirk 1:
+# gitlab-ci-local doesn't like worktrees. rsync to a tmpdir, git init,
+# run gitlab-ci-local, then rsync .gitlab-ci-local back for caching
+if [[ -f .git ]]; then
+ srcdir=$(realpath .)
+ tmpdir=$(mktemp -d)
+ cleanup+=("$tmpdir")
+ rsync -a --exclude=.git . "$tmpdir/"
+ cd "$tmpdir"
+ orig_gitdir=$(sed -n 's/^gitdir: //p' "$srcdir/.git")
+ git init -q
+ cp "$orig_gitdir/index" .git/index
+ CLONE_SUBMODULES=false
+ git submodule update --init --depth 1 || true
+ rm -f .gitmodules
+ git add -A
+ git -c gc.autoDetach=false commit -q -m "local ci run"
+fi
+
+# Quirk 2:
+# --container-executable values like "podman --remote" need a wrapper script
+if [[ "$(echo "$CONTAINER_EXECUTABLE")" = *" "* ]]; then
+ wrapper="$(mktemp)"
+ echo -e '#!/bin/sh\nexec %s "$@"\n' "$CONTAINER_EXECUTABLE" > "$wrapper"
+ chmod +x "$wrapper"
+ cleanup+=("$wrapper")
+ CONTAINER_EXECUTABLE="$wrapper"
+fi
+
+# generate a gitlab-ci-local compatible .gitlab-ci-local.yml overrides file
+{
+ echo '# auto-generated by devel/ci-local.sh — do not edit.\n'
+
+ # Quirk 3:
+ # add explicit "paths:" to "artifacts: untracked: true" without them
+ yq eval '
+ to_entries[] |
+ select(.value | has("artifacts")) |
+ select(.value.artifacts.untracked == true) |
+ select(.value.artifacts | has("paths") | not) |
+ .key
+ ' .gitlab-ci.yml | while IFS= read -r key; do
+ echo -e "${key}:\n artifacts:\n paths:\n - ./\n"
+ done
+
+ # Optional: skip commit-check for local runs by default, it's annoying
+ [[ "$SKIP_COMMIT_CHECK" = true ]] && \
+ echo -e 'commit-check:\n rules:\n - when: never\n'
+
+ [[ "$CLONE_SUBMODULES" = false ]] && \
+ echo -e 'variables:\n GIT_SUBMODULE_STRATEGY: none\n'
+} > .gitlab-ci-local.yml
+
+# run gitlab-ci-local with some reasonable defaults
+gitlab-ci-local \
+ --container-executable "$CONTAINER_EXECUTABLE" \
+ --ignore-predefined-vars CI_REGISTRY \
+ --variable "CI_REGISTRY=$CI_REGISTRY" \
+ --no-artifacts-to-source \
+ "$@"
+exit $?