Skip to content
Merged
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 .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ shell-unit-tests:
image: ${PREPARE_IMAGE}
script:
- bash .gitlab/scripts/tests/includes_test.sh
- bash .gitlab/scripts/tests/test_check_stack_top.sh
- bash .gitlab/dd-trace-integration/tests/post_pr_comment_test.sh

# Shared version detection used by benchmarks and reliability pipelines
Expand Down
144 changes: 118 additions & 26 deletions .gitlab/benchmarks/.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
variables:
DD_OCTO_STS_IMAGE: registry.ddbuild.io/images/dd-octo-sts-ci-base:2025.06-1

# Bridge job: triggers the BP pipeline and blocks until it completes.
# Bridge jobs cannot appear in other jobs' needs: — downstream jobs use
# stage ordering (post-benchmarks stage runs after benchmarks stage).
benchmarks-trigger:
stage: benchmarks
# Bridge jobs cannot have before_script, so CANCELLED is checked via rules.
# interruptible: false prevents orphaning the BP downstream pipeline on push.
interruptible: false
needs:
- job: get-versions
artifacts: true
- job: deploy-artifact
artifacts: false
# Benchmarks run only for the top of a PR stack. Stacked PRs chain
# head -> base (PR_n's base is PR_{n-1}'s head branch), so a branch with an
# open PR using it as base is below another PR in the stack. A bridge job
# cannot run the check itself (trigger jobs have no script) and dotenv
# variables cannot drive `rules`, so the decision is baked into which child
# pipeline YAML generate-benchmarks-child-pipeline emits:
# - top of stack (or check failed open): real child pipeline that triggers
# the BP pipeline (.gitlab/benchmarks/child.gitlab-ci.yml)
# - below another PR: noop child pipeline that only logs the skip
# This mirrors the generate-reliability-child-pipeline / run-reliability-tests
# pattern in .gitlab-ci.yml.

# Shared skip conditions for the benchmarks jobs below. Both jobs must
# always coexist (run-benchmarks hard-needs the generate job's artifact), so
# the never-conditions MUST stay identical for both — keep them only here.
# This mirrors the .skip-on-release pattern in .gitlab/common.yml.
.benchmarks-skip-rules:
rules:
- if: '$CANCELLED == "true"'
when: never
Expand All @@ -24,26 +28,114 @@ benchmarks-trigger:
when: never
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never

# Decide whether benchmarks run for this branch and generate the child
# pipeline YAML accordingly.
generate-benchmarks-child-pipeline:
stage: benchmarks
tags: ["arch:amd64"]
image: $DD_OCTO_STS_IMAGE
id_tokens:
DDOCTOSTS_ID_TOKEN:
aud: dd-octo-sts
needs: []
rules:
- !reference [.benchmarks-skip-rules, rules]
# Runs automatically in web pipelines too: run-benchmarks (manual on web)
# hard-needs this job's artifact, so a manual-only generate job would
# break the manual trigger path with an unmet-needs artifact error.
- when: on_success
script:
# Single canonical parse of the script's contract output: stdout is
# exactly one line "RUN_BENCHMARKS=<value>"; stderr (diagnostics) passes
# through to the job log. Fail open unless the value is unambiguously
# "false" — an empty, missing, or corrupted parse must never skip
# benchmarks.
- |
RUN_BENCHMARKS=$(bash .gitlab/benchmarks/check-stack-top.sh | tail -n 1 | sed -n 's/^RUN_BENCHMARKS=//p' | tr -d '[:space:]')
case "$RUN_BENCHMARKS" in
false) RUN_BENCHMARKS=false ;;
*) RUN_BENCHMARKS=true ;;
esac
echo "RUN_BENCHMARKS=$RUN_BENCHMARKS"
if [ "$RUN_BENCHMARKS" = "true" ]; then
echo "Branch is the top of its PR stack — benchmarks enabled"
cp .gitlab/benchmarks/child.gitlab-ci.yml generated-benchmarks.yml
else
echo "Branch is below another PR in its stack — benchmarks skipped"
cat > generated-benchmarks.yml << 'NOOP'
skip-benchmarks:
image: registry.ddbuild.io/images/benchmarking-platform-tools-ubuntu:newest
tags: ["arch:amd64"]
script:
- echo "Branch is not the top of its PR stack — skipping benchmarks"
rules:
- when: always
NOOP
fi
artifacts:
paths:
- generated-benchmarks.yml
expire_in: 1 day

# Bridge job: triggers the benchmarks child pipeline and blocks until it
# completes.
#
# Variable forwarding to the child pipeline:
# - CANDIDATE_VERSION / BASELINE_VERSION: get-versions produces them as a
# dotenv artifact; listing get-versions in needs injects them into THIS
# job's variable context, and redeclaring them here makes them
# trigger-job variables, which are forwarded by default (yaml_variables).
# - BENCHMARK_ITERATIONS / BENCHMARK_MODES: manual pipeline variables,
# forwarded via trigger:forward: pipeline_variables: true.
# - PARENT_PIPELINE_ID / PARENT_COMMIT_SHA / PARENT_COMMIT_BRANCH: the
# child pipeline's own CI_PIPELINE_ID/CI_COMMIT_* refer to the child,
# so the parent values must be passed explicitly.
run-benchmarks:
stage: benchmarks
# Bridge jobs cannot have before_script, so CANCELLED is checked via rules.
# interruptible: false prevents orphaning the BP downstream pipeline on push.
interruptible: false
needs:
- job: generate-benchmarks-child-pipeline
artifacts: true
- job: get-versions
artifacts: true
# Benchmarks benchmark the artifact published by deploy-artifact; without
# this gate the BP pipeline could start before the artifact exists.
# optional: true keeps the need satisfiable on branches where
# deploy-artifact is legitimately skipped (e.g. release branches), same
# as run-reliability-tests in .gitlab-ci.yml.
- job: deploy-artifact
artifacts: false
optional: true
variables:
PARENT_PIPELINE_ID: "$CI_PIPELINE_ID"
PARENT_COMMIT_SHA: "$CI_COMMIT_SHA"
# GitLab trigger variables do not evaluate Bash ${VAR:-default}
# expansions, so a fallback must be chosen as the single variable that is
# always populated: CI_COMMIT_REF_NAME (unlike CI_COMMIT_BRANCH) is set
# for detached-HEAD trigger/api pipelines too.
PARENT_COMMIT_BRANCH: "$CI_COMMIT_REF_NAME"
# Redeclared from the get-versions dotenv artifact (via needs) so they
# reach the child pipeline as trigger-job variables — see the comment
# above the job.
CANDIDATE_VERSION: "$CURRENT_VERSION"
BASELINE_VERSION: "$PREVIOUS_VERSION"
rules:
- !reference [.benchmarks-skip-rules, rules]
- if: '$CI_PIPELINE_SOURCE == "web"'
when: manual
allow_failure: true
# Run automatically and non-blocking on any other source (push/trigger/api/etc.)
- when: on_success
allow_failure: true
variables:
CANDIDATE_VERSION: "${CURRENT_VERSION}"
BASELINE_VERSION: "${PREVIOUS_VERSION}"
BENCHMARK_ITERATIONS: "${BENCHMARK_ITERATIONS:-5}"
BENCHMARK_MODES: "${BENCHMARK_MODES:-cpu,wall,alloc,memleak}"
DDPROF_COMMIT_SHA: "${CI_COMMIT_SHA}"
DDPROF_COMMIT_BRANCH: "${CI_COMMIT_BRANCH}"
UPSTREAM_PROJECT_NAME: "java-profiler"
UPSTREAM_BRANCH: "${CI_COMMIT_BRANCH}"
UPSTREAM_PIPELINE_ID: "${CI_PIPELINE_ID}"
trigger:
project: DataDog/apm-reliability/benchmarking-platform
branch: java-profiler
include:
Comment thread
jbachorik marked this conversation as resolved.
- artifact: generated-benchmarks.yml
job: generate-benchmarks-child-pipeline
strategy: depend
forward:
pipeline_variables: true


publish-benchmark-gh-pages:
Expand Down
127 changes: 127 additions & 0 deletions .gitlab/benchmarks/check-stack-top.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/bin/bash

# check-stack-top.sh - Decide whether benchmarks should run for this branch.
#
# A branch is the top of its PR stack when no other open PR uses it as base
# branch. Stacked PRs chain head -> base (PR_n's base is PR_{n-1}'s head
# branch), so an open PR with base == <this branch> means this branch sits
# below another PR in the stack. Benchmarks only run for the top of a stack:
# intermediate commits get no BP pipeline.
#
# Usage: check-stack-top.sh [branch]
# branch defaults to $CI_COMMIT_BRANCH.
# Output: exactly one line "RUN_BENCHMARKS=true" or "RUN_BENCHMARKS=false" on
# stdout (diagnostics go to stderr).
#
# Fail-open: on any API/auth/parsing error the decision is RUN_BENCHMARKS=true
# so a GitHub outage can never silently disable benchmark coverage.
#
# Authentication mirrors .gitlab/common/lookup-pr.sh: Octo-STS token when
# available, GITHUB_TOKEN fallback, anonymous as last resort. The token is
# passed to curl via a header file, never as a command-line argument (argv is
# world-readable on shared runners via /proc/*/cmdline).

set -uo pipefail

BRANCH="${1:-${CI_COMMIT_BRANCH:-}}"
REPO="DataDog/java-profiler"

debug() { echo "[DEBUG] $*" >&2; }

result() {
echo "RUN_BENCHMARKS=$1"
debug "decision: RUN_BENCHMARKS=$1 ($2)"
exit 0
}

# main/master are the stack root: open PRs target them as base by
# definition, so the base-branch query below would always report "not top".
if [ -z "${BRANCH}" ] || [ "${BRANCH}" = "main" ] || [ "${BRANCH}" = "master" ]; then
result "true" "branch is ${BRANCH:-<empty>}"
fi

# Authentication: pre-existing GITHUB_TOKEN env var, refreshed via dd-octo-sts
# when available (same scheme as lookup-pr.sh)
GITHUB_TOKEN="${GITHUB_TOKEN:-}"
if command -v dd-octo-sts >/dev/null 2>&1 && [ -n "${DDOCTOSTS_ID_TOKEN:-}" ]; then
debug "Attempting to get token via Octo-STS..."
if TOKEN_OUTPUT=$(dd-octo-sts token --scope "${REPO}" --policy async-profiler-build.ci 2>/tmp/dd-octo-sts-stack-top-error.log) && [ -n "${TOKEN_OUTPUT}" ]; then
GITHUB_TOKEN="${TOKEN_OUTPUT}"
debug "Got GitHub token via Octo-STS"
else
debug "Failed to get token via Octo-STS, falling back"
fi
fi
# Normalize: strip CR/LF/outer whitespace and reject values that are not a
# bare token — a multi-line or decorated value would be interpolated into an
# HTTP header (injected headers or guaranteed auth failure).
GITHUB_TOKEN=$(printf '%s' "${GITHUB_TOKEN}" | tr -d '\r\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if [ -n "${GITHUB_TOKEN}" ] && ! printf '%s' "${GITHUB_TOKEN}" | grep -qE '^[A-Za-z0-9_.=-]+$'; then
debug "Token has unexpected shape — ignoring it"
GITHUB_TOKEN=""
fi

# URL-encode the branch name (/ -> %2F, etc.) - same approach as lookup-pr.sh
url_encode() {
local string="$1"
if command -v jq >/dev/null 2>&1; then
printf '%s' "$string" | jq -sRr @uri
else
# % must be encoded first so the encodings added below are not re-encoded
printf '%s' "$string" | sed 's/%/%25/g; s|/|%2F|g; s/ /%20/g; s/#/%23/g; s/+/%2B/g; s/&/%26/g; s/?/%3F/g; s/=/%3D/g; s/:/%3A/g; s/;/%3B/g; s/@/%40/g'
fi
}

ENCODED_BRANCH=$(url_encode "${BRANCH}")
API_URL="https://api.github.com/repos/${REPO}/pulls?state=open&base=${ENCODED_BRANCH}&per_page=1"
debug "API URL: ${API_URL}"

# Pass the credential out-of-band: curl reads the header from a 0600 temp file
# instead of receiving it as a world-readable argv element.
AUTH_HEADER_FILE=""
cleanup() { [ -n "${AUTH_HEADER_FILE}" ] && rm -f "${AUTH_HEADER_FILE}"; }
trap cleanup EXIT
if [ -n "${GITHUB_TOKEN}" ]; then
AUTH_HEADER_FILE=$(mktemp "${TMPDIR:-/tmp}/sphinx-stack-top-hdr.XXXXXX")
chmod 600 "${AUTH_HEADER_FILE}"
printf 'Authorization: token %s\n' "${GITHUB_TOKEN}" > "${AUTH_HEADER_FILE}"
debug "Using authenticated request (header via file)"
else
debug "Using anonymous request (may be rate limited)"
fi

BODY_FILE=$(mktemp "${TMPDIR:-/tmp}/sphinx-stack-top-body.XXXXXX")
HTTP_CODE=$(curl -s -o "${BODY_FILE}" -w '%{http_code}' --max-time 10 \
${AUTH_HEADER_FILE:+-H "@${AUTH_HEADER_FILE}"} \
-H "Accept: application/vnd.github+json" \
"${API_URL}" 2>/dev/null)
CURL_EXIT=$?
response=$(cat "${BODY_FILE}" 2>/dev/null)
rm -f "${BODY_FILE}"

if [ "${CURL_EXIT}" -ne 0 ]; then
result "true" "curl failed with exit ${CURL_EXIT} (fail-open)"
fi
if [ "${HTTP_CODE}" != "200" ]; then
# HTTP 403 with a JSON error object is the anonymous rate-limit response;
# surfacing the code here makes that failure observable instead of silent.
result "true" "GitHub API returned HTTP ${HTTP_CODE} (fail-open)"
fi

debug "API response length: ${#response} chars"
debug "API response preview: ${response:0:200}"

if ! command -v jq >/dev/null 2>&1; then
result "true" "jq not available (fail-open)"
fi

if ! echo "${response}" | jq -e 'type == "array"' >/dev/null 2>&1; then
result "true" "response is not a JSON array (API error, fail-open)"
fi

STACKED_ON_TOP=$(echo "${response}" | jq 'length')
if [ "${STACKED_ON_TOP}" -gt 0 ]; then
result "false" "${STACKED_ON_TOP} open PR(s) use ${BRANCH} as base branch"
fi

result "true" "no open PR stacks on ${BRANCH}"
64 changes: 64 additions & 0 deletions .gitlab/benchmarks/child.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Benchmarks child pipeline.
#
# Triggered by run-benchmarks in the parent pipeline (generated copy of this
# file is passed via `trigger: include: artifact`). Created only when
# generate-benchmarks-child-pipeline decided benchmarks should run — either
# because the branch is the top of its PR stack (check-stack-top.sh) or the
# check failed open.
#
# Variables forwarded from the parent pipeline via run-benchmarks:
# - CANDIDATE_VERSION / BASELINE_VERSION: declared in run-benchmarks'
# `variables:` block (values from the get-versions dotenv artifact), so
# they arrive as trigger-job variables — the documented forwarding path.
# - BENCHMARK_ITERATIONS / BENCHMARK_MODES (manual pipeline variables, via
# trigger:forward: pipeline_variables: true)
# - PARENT_PIPELINE_ID / PARENT_COMMIT_SHA / PARENT_COMMIT_BRANCH: set
# explicitly by run-benchmarks because CI_PIPELINE_ID/CI_COMMIT_* inside
# this child pipeline refer to the child, not the parent.
---
stages:
- benchmarks

# Fail fast when the version variables did not survive the parent -> child
# hop: without this guard the BP pipeline would consume empty versions.
benchmarks-inputs-guard:
stage: benchmarks
image: registry.ddbuild.io/images/benchmarking-platform-tools-ubuntu:newest
tags: ["arch:amd64"]
needs: []
rules:
# The child pipeline is independently retryable in the UI; a manual retry
# must not bypass the parent-level CANCELLED gate and start a BP run.
- if: '$CANCELLED == "true"'
when: never
- when: on_success
script:
- '[ -n "${CANDIDATE_VERSION:-}" ] || { echo "CANDIDATE_VERSION is empty — version forwarding from the parent pipeline failed"; exit 1; }'
- '[ -n "${BASELINE_VERSION:-}" ] || { echo "BASELINE_VERSION is empty — version forwarding from the parent pipeline failed"; exit 1; }'
- echo "CANDIDATE_VERSION=${CANDIDATE_VERSION} BASELINE_VERSION=${BASELINE_VERSION}"

# Bridge job: triggers the BP pipeline and blocks until it completes.
# Bridge jobs cannot appear in other jobs' needs: — downstream jobs use
# stage ordering (post-benchmarks stage in the parent pipeline runs after
# the benchmarks stage).
benchmarks-trigger:
Comment thread
jbachorik marked this conversation as resolved.
stage: benchmarks
# Bridge jobs cannot have before_script, so CANCELLED is checked via the
# guard job's rules above.
# interruptible: false prevents orphaning the BP downstream pipeline on push.
interruptible: false
needs: [benchmarks-inputs-guard]
variables:
CANDIDATE_VERSION: "${CANDIDATE_VERSION}"
BASELINE_VERSION: "${BASELINE_VERSION}"
BENCHMARK_ITERATIONS: "${BENCHMARK_ITERATIONS:-5}"
BENCHMARK_MODES: "${BENCHMARK_MODES:-cpu,wall,alloc,memleak}"
DDPROF_COMMIT_SHA: "${PARENT_COMMIT_SHA}"
DDPROF_COMMIT_BRANCH: "${PARENT_COMMIT_BRANCH}"
UPSTREAM_PROJECT_NAME: "java-profiler"
UPSTREAM_BRANCH: "${PARENT_COMMIT_BRANCH}"
UPSTREAM_PIPELINE_ID: "${PARENT_PIPELINE_ID}"
trigger:
project: DataDog/apm-reliability/benchmarking-platform
branch: java-profiler
strategy: depend
Loading
Loading