Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions gitlab-ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tail -n 200 is hardcoded in action.yml, so there's no input for it. Please expose it as an input (e.g. trace-tail-lines, defaulting to 200).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the README with the recent changes.


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.
33 changes: 33 additions & 0 deletions gitlab-ci/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -64,3 +72,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() && inputs.expose-error-logs == 'true'
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 ${{ inputs.trace-tail-lines }} "$trace"
echo "::${tok}::"
echo "::endgroup::"
done
106 changes: 106 additions & 0 deletions gitlab-ci/fetch_gitlab_logs.sh
Original file line number Diff line number Diff line change
@@ -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/<your-repo>
# 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