From ac8a2e13f0f99bef678224f61fe7cf1b65575a08 Mon Sep 17 00:00:00 2001 From: diaco Date: Wed, 27 May 2026 18:46:44 +0200 Subject: [PATCH 1/2] gitlab-ci: Surface failed GitLab job traces in the Actions log on failure On pipeline failure, fetch the traces of failed GitLab jobs via the API and tail them into the GitHub Actions log, grouped per job, so failures can be debugged without direct GitLab access. Untrusted trace content is wrapped in ::stop-commands:: with a random token. The API token is passed via a 0600 curl config file rather than argv, and trace filenames are suffixed with the job id to avoid collisions between same-named jobs. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + gitlab-ci/README.md | 2 + gitlab-ci/action.yml | 25 ++++++++ gitlab-ci/fetch_gitlab_logs.sh | 106 +++++++++++++++++++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 gitlab-ci/fetch_gitlab_logs.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 9afb56c..eedae00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project (post v2.1.0) adheres to [Semantic Versioning](http://semver.or ## Unreleased ### Added +- `gitlab-ci`: On pipeline failure, fetch traces of failed GitLab jobs and tail them into the GitHub Actions log. - `slang`: Add `reviewdog-name` variable to optionally pass a name for the reviewdog check. ## 2.5.0 - 2026-03-31 diff --git a/gitlab-ci/README.md b/gitlab-ci/README.md index 5de3267..b25f0d5 100644 --- a/gitlab-ci/README.md +++ b/gitlab-ci/README.md @@ -39,4 +39,6 @@ jobs: Optional inputs controlling the Gitlab API version and timeouts are available; see `action.yml`. +On pipeline failure, the action automatically fetches the traces of failed GitLab jobs and tails them into the GitHub Actions log (grouped per job), so you can see what went wrong without leaving the Actions UI and without needing personal access to the GitLab instance (the traces are fetched using the same mirror `token` the action already uses). Only the last 200 lines of each job's trace are printed (the failure is usually at the end); see the full trace in GitLab or adjust the parameter in the .yml if you need more context. + Be sure to add a `.gitlab-ci.yml` to your repo; otherwise, the action will time out waiting for a pipeline to spawn on new commits, resulting in failure. diff --git a/gitlab-ci/action.yml b/gitlab-ci/action.yml index 90a2a24..9c82935 100644 --- a/gitlab-ci/action.yml +++ b/gitlab-ci/action.yml @@ -64,3 +64,28 @@ runs: export SHA=${{ github.sha }} if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then export SHA=${{ github.event.pull_request.head.sha }}; fi uv run --with requests ${{ github.action_path }}/gitlab-ci.py $SHA ${{ inputs.token }} ${{ inputs.domain }} ${{ inputs.repo }} ${{ inputs.api-version }} ${{ inputs.retry-count }} ${{ inputs.retry-period }} ${{ inputs.poll-count }} ${{ inputs.poll-period }} + - name: Surface and print GitLab CI error logs on failure + if: failure() + shell: bash + env: + GITLAB_TOKEN: ${{ inputs.token }} + GITLAB_DOMAIN: ${{ inputs.domain }} + GITLAB_REPO: ${{ inputs.repo }} + API_VERSION: ${{ inputs.api-version }} + COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + OUT_DIR: gitlab-logs + run: | + bash ${{ github.action_path }}/fetch_gitlab_logs.sh + shopt -s nullglob + for trace in "$OUT_DIR"/*.trace; do + job=$(basename "$trace" .trace) + echo "::group:: FAILED: ${job}" + # Trace content is untrusted; disable Actions workflow-command parsing so + # lines like "::error::" in the trace are printed literally, not executed. + # The random token prevents the trace from re-enabling parsing early. + tok=$(uuidgen) + echo "::stop-commands::${tok}" + tail -n 200 "$trace" + echo "::${tok}::" + echo "::endgroup::" + done diff --git a/gitlab-ci/fetch_gitlab_logs.sh b/gitlab-ci/fetch_gitlab_logs.sh new file mode 100644 index 0000000..81d3dab --- /dev/null +++ b/gitlab-ci/fetch_gitlab_logs.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Copyright 2026 ETH Zurich and University of Bologna. +# Licensed under the Apache License, Version 2.0, see LICENSE.APACHE for details. +# SPDX-License-Identifier: Apache-2.0 +# +# Fetch failed-job traces from the mirrored GitLab pipeline and surface them +# in the GitHub Actions console. Intended to run in a GitHub workflow step +# gated on `if: failure()` after the pulp-actions gitlab-ci check. Always +# exits 0 — the original gitlab-ci step is the one that fails the workflow; +# this script is purely informational. +# +# Required environment: +# GITLAB_TOKEN Token with read_api scope (same token used by the mirror). +# GITLAB_DOMAIN e.g. iis-git.ee.ethz.ch +# GITLAB_REPO e.g. github-mirror/ +# COMMIT_SHA GitHub commit SHA mirrored to GitLab. +# OUT_DIR Output directory (created if missing). +# API_VERSION GitLab API version (defaults to v4). + +set -u +set -o pipefail + +: "${GITLAB_TOKEN:?GITLAB_TOKEN is required}" +: "${GITLAB_DOMAIN:?GITLAB_DOMAIN is required}" +: "${GITLAB_REPO:?GITLAB_REPO is required}" +: "${COMMIT_SHA:?COMMIT_SHA is required}" +: "${OUT_DIR:=gitlab-logs}" +: "${API_VERSION:=v4}" + +mkdir -p "$OUT_DIR" + +API="https://${GITLAB_DOMAIN}/api/${API_VERSION}" +PROJECT_ID="${GITLAB_REPO//\//%2F}" + +# Pass the token via a 0600 curl config file rather than on the command line, +# so it does not appear in argv (visible to other processes via `ps`). +CURL_CONFIG=$(mktemp) +trap 'rm -f "$CURL_CONFIG"' EXIT +chmod 600 "$CURL_CONFIG" +printf 'header = "PRIVATE-TOKEN: %s"\n' "$GITLAB_TOKEN" > "$CURL_CONFIG" +CURL=(curl -fsSL --retry 3 --retry-delay 5 -K "$CURL_CONFIG") + +sanitize() { + # Replace characters that are awkward in filenames. + echo "$1" | tr '/ :' '___' +} + +# --- 1. Find the newest pipeline for this commit --- +PIPELINE_JSON="" +for attempt in 1 2 3 4 5; do + if PIPELINE_JSON=$("${CURL[@]}" \ + "${API}/projects/${PROJECT_ID}/pipelines?sha=${COMMIT_SHA}&order_by=id&sort=desc&per_page=1") \ + && [ "$(echo "$PIPELINE_JSON" | jq 'length')" -gt 0 ]; then + break + fi + echo "::warning::No GitLab pipeline found for ${COMMIT_SHA} yet (attempt ${attempt}/5), retrying in 10s…" >&2 + PIPELINE_JSON="" + sleep 10 +done + +if [ -z "$PIPELINE_JSON" ] || [ "$(echo "$PIPELINE_JSON" | jq 'length')" -eq 0 ]; then + echo "::warning::Gave up looking for a GitLab pipeline matching ${COMMIT_SHA}. Not fetching logs." + exit 0 +fi + +PIPELINE_ID=$(echo "$PIPELINE_JSON" | jq -r '.[0].id') +PIPELINE_URL=$(echo "$PIPELINE_JSON" | jq -r '.[0].web_url') +echo "Inspecting GitLab pipeline ${PIPELINE_ID}: ${PIPELINE_URL}" + +# --- 2. Paginate through jobs --- +JOBS_JSON="[]" +page=1 +while :; do + PAGE_JSON=$("${CURL[@]}" \ + "${API}/projects/${PROJECT_ID}/pipelines/${PIPELINE_ID}/jobs?per_page=100&page=${page}") || { + echo "::warning::Failed to fetch jobs page ${page}; stopping pagination." >&2 + break + } + count=$(echo "$PAGE_JSON" | jq 'length') + [ "$count" -eq 0 ] && break + JOBS_JSON=$(jq -s '.[0] + .[1]' <(echo "$JOBS_JSON") <(echo "$PAGE_JSON")) + [ "$count" -lt 100 ] && break + page=$((page + 1)) +done + +# --- 3. For each failed job: fetch its trace --- +FAILED_COUNT=0 +while IFS=$'\t' read -r job_id job_name job_stage; do + [ -z "$job_id" ] && continue + FAILED_COUNT=$((FAILED_COUNT + 1)) + # Suffix with the (unique) job id so retried jobs or jobs whose stage+name + # sanitize to the same string don't overwrite each other's trace. + slug="$(sanitize "$job_stage")__$(sanitize "$job_name")__${job_id}" + trace_path="${OUT_DIR}/${slug}.trace" + + if ! err=$("${CURL[@]}" "${API}/projects/${PROJECT_ID}/jobs/${job_id}/trace" \ + -o "$trace_path" 2>&1); then + echo "::warning::Could not fetch trace for job ${job_id} (${job_name}): ${err}" >&2 + echo "(trace unavailable)" > "$trace_path" + fi + + echo "Trace: ${trace_path} (job=${job_name}, stage=${job_stage})" +done < <(echo "$JOBS_JSON" | jq -r '.[] | select(.status == "failed") | [.id, .name, .stage] | @tsv') + +echo "Fetched ${FAILED_COUNT} failed job(s) from pipeline ${PIPELINE_URL}" +exit 0 From 7e59cd3a33306d76e43cce24128abbe288680c1b Mon Sep 17 00:00:00 2001 From: diaco Date: Thu, 11 Jun 2026 17:30:17 +0200 Subject: [PATCH 2/2] gitlab-ci: Make error log surfacing opt-in and expose tail-lines input Add `expose-error-logs` (default: false) to avoid leaking sensitive trace content by default, and `trace-tail-lines` (default: 200) to make the tail length configurable. Co-Authored-By: Claude Sonnet 4.6 --- gitlab-ci/action.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/gitlab-ci/action.yml b/gitlab-ci/action.yml index 9c82935..269ce37 100644 --- a/gitlab-ci/action.yml +++ b/gitlab-ci/action.yml @@ -39,6 +39,14 @@ inputs: description: 'Period of polls in seconds while pipeline is running' required: true default: 10 + expose-error-logs: + description: 'Fetch and surface failed job traces in the Actions log on failure (opt-in; disable if traces contain sensitive information)' + required: false + default: 'false' + trace-tail-lines: + description: 'Number of lines to show from the end of each failed job trace' + required: false + default: 200 runs: using: "composite" @@ -65,7 +73,7 @@ runs: if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then export SHA=${{ github.event.pull_request.head.sha }}; fi uv run --with requests ${{ github.action_path }}/gitlab-ci.py $SHA ${{ inputs.token }} ${{ inputs.domain }} ${{ inputs.repo }} ${{ inputs.api-version }} ${{ inputs.retry-count }} ${{ inputs.retry-period }} ${{ inputs.poll-count }} ${{ inputs.poll-period }} - name: Surface and print GitLab CI error logs on failure - if: failure() + if: failure() && inputs.expose-error-logs == 'true' shell: bash env: GITLAB_TOKEN: ${{ inputs.token }} @@ -85,7 +93,7 @@ runs: # The random token prevents the trace from re-enabling parsing early. tok=$(uuidgen) echo "::stop-commands::${tok}" - tail -n 200 "$trace" + tail -n ${{ inputs.trace-tail-lines }} "$trace" echo "::${tok}::" echo "::endgroup::" done