Skip to content

semconv-parity

semconv-parity #14

# Keeps tests/fixtures/semconv.json — the mirror of rius-sdk-python's
# semconv.py — honest, and decides what a human has to look at.
#
# TWO KINDS OF DRIFT, and they deserve different treatment. Regenerating the
# fixture is self-checking, because tests/semconv.test.ts walks the FIXTURE's
# keys and compares each against src/semconv.ts:
#
# - The suite stays GREEN -> Python and this SDK already agree and only the
# committed mirror lagged. Nothing to decide. That opens a PR.
# - The suite goes RED -> Python has a constant this SDK lacks, or a value
# differs. A changed attribute key is a WIRE change, and the resolution is
# a judgement call: port it, declare it in PYTHON_ONLY, or fix Python
# instead. A bot cannot pick. That opens an issue.
#
# Previously every drift opened an issue. #25 is what that cost: Python had
# gained SERVICE_INSTANCE_ID, src/semconv.ts already exported it with the same
# value, and the one-line fixture refresh sat as an open issue for days.
#
# On pull_request nothing is filed either way — drift just fails the job, so a
# PR touching semconv cannot land with a stale mirror.
name: semconv-parity
on:
pull_request:
paths: ["src/semconv.ts", "tests/fixtures/semconv.json", "scripts/gen-semconv-fixture.mjs"]
schedule:
- cron: "0 6 * * 1"
# For verifying the check end-to-end (it silently skips without
# SEMCONV_SOURCE_TOKEN, so a green run is not proof by itself).
workflow_dispatch:
jobs:
parity:
runs-on: ubuntu-latest
permissions:
contents: write # the bot branch carrying a regenerated fixture
issues: write
pull-requests: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
- name: Check for a source-repo token
id: check
run: |
if [ -z "${{ secrets.SEMCONV_SOURCE_TOKEN }}" ]; then
echo "::warning::SEMCONV_SOURCE_TOKEN is not set; skipping the semconv parity check against rius-sdk-python."
echo "has_token=false" >> "$GITHUB_OUTPUT"
else
echo "has_token=true" >> "$GITHUB_OUTPUT"
fi
- name: Regenerate the fixture from the Python SDK
if: steps.check.outputs.has_token == 'true'
env:
SEMCONV_SOURCE_TOKEN: ${{ secrets.SEMCONV_SOURCE_TOKEN }}
run: node scripts/gen-semconv-fixture.mjs
# Records the drift instead of failing on it, so the classify step below
# can decide what it means. The job still fails at the end on
# pull_request.
- name: Detect drift
if: steps.check.outputs.has_token == 'true'
id: drift
run: |
if git diff --quiet tests/fixtures/semconv.json; then
echo "drifted=false" >> "$GITHUB_OUTPUT"
echo "no drift" >> "$GITHUB_STEP_SUMMARY"
else
echo "drifted=true" >> "$GITHUB_OUTPUT"
{
echo 'diff<<SEMCONV_EOF'
git diff -- tests/fixtures/semconv.json
echo 'SEMCONV_EOF'
} >> "$GITHUB_OUTPUT"
{ echo '```diff'; git diff -- tests/fixtures/semconv.json; echo '```'; } \
>> "$GITHUB_STEP_SUMMARY"
fi
- name: Install dependencies
if: steps.drift.outputs.drifted == 'true'
run: npm ci
# The classification. A green suite means the regenerated mirror is
# consistent with src/semconv.ts and needs no human judgement.
- name: Classify the drift
if: steps.drift.outputs.drifted == 'true'
id: classify
run: |
if npx vitest run tests/semconv.test.ts; then
echo "mechanical=true" >> "$GITHUB_OUTPUT"
echo "drift is mechanical: the regenerated fixture keeps the suite green" >> "$GITHUB_STEP_SUMMARY"
else
echo "mechanical=false" >> "$GITHUB_OUTPUT"
echo "drift needs review: the regenerated fixture fails the parity test" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Open or update the fixture-refresh PR
if: >-
steps.classify.outputs.mechanical == 'true' &&
github.event_name != 'pull_request'
env:
# A PAT, not github.token: PRs opened by GITHUB_TOKEN do not trigger
# `pull_request` workflows at all, so the PR would arrive with no CI
# on the very check it exists to satisfy. Falls back so the step
# still works — a reviewer then has to kick CI by hand.
#
# Fine-grained, scoped to this repo, and it needs exactly two
# permissions: Pull requests (write) to open and update the PR, and
# Contents (write) for the branch push below. Not Issues — the
# review-issue step deliberately stays on GITHUB_TOKEN.
GH_TOKEN: ${{ secrets.SEMCONV_PR_TOKEN || secrets.GITHUB_TOKEN }}
BRANCH: bot/semconv-fixture
DIFF: ${{ steps.drift.outputs.diff }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "$BRANCH"
git commit -am "chore: refresh the semconv fixture from the Python SDK"
# Push with the SAME token that opens the PR, not the one
# actions/checkout wired into the remote. A force-push by
# GITHUB_TOKEN fires `synchronize` as the bot, and events triggered
# by GITHUB_TOKEN start no workflow run — so every weekly refresh
# after the first would quietly arrive unchecked.
git push -f "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$BRANCH"
title="chore: refresh the semconv fixture from the Python SDK"
body=$(printf 'The weekly parity check found the fixture behind `rius-sdk-python`:\n\n```diff\n%s\n```\n\nThis is the MECHANICAL class of drift: `src/semconv.ts` already agrees with Python and only the committed mirror lagged, so the regenerated fixture keeps `tests/semconv.test.ts` green. Had Python added a constant this SDK genuinely lacks, or changed a value, the suite would be red and this would have been filed as an issue instead — that is a wire change and needs a human.\n\nRe-run the workflow to refresh this PR; the branch is force-pushed.\n' "$DIFF")
# REST rather than `gh pr create`: the porcelain still requests the
# deprecated projectCards GraphQL field and hard-fails on some gh
# versions.
existing=$(gh api "repos/${GITHUB_REPOSITORY}/pulls" \
-X GET -f state=open -f head="${GITHUB_REPOSITORY%%/*}:${BRANCH}" \
-q '.[0].number // empty')
if [ -z "$existing" ]; then
num=$(gh api "repos/${GITHUB_REPOSITORY}/pulls" -X POST \
-f title="$title" -f body="$body" -f head="$BRANCH" -f base=main -q '.number')
echo "opened PR #${num}" >> "$GITHUB_STEP_SUMMARY"
else
gh api "repos/${GITHUB_REPOSITORY}/pulls/${existing}" -X PATCH \
-f title="$title" -f body="$body" >/dev/null
echo "updated PR #${existing}" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Open or update the review issue
if: >-
steps.classify.outputs.mechanical == 'false' &&
github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DIFF: ${{ steps.drift.outputs.diff }}
run: |
set -euo pipefail
title="semconv drifted from rius-sdk-python"
body=$(printf 'The parity check found differences the regenerated fixture cannot resolve on its own: `tests/semconv.test.ts` fails against it.\n\n```diff\n%s\n```\n\nA changed attribute key is a WIRE change. Resolve it deliberately — port the constant to `src/semconv.ts`, declare it in the `PYTHON_ONLY` set in `tests/semconv.test.ts` as intentionally unported, or fix `rius-sdk-python` if that is the side that is wrong.\n\n[Run](%s/%s/actions/runs/%s)\n' \
"$DIFF" "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID")
# Comment on the standing issue rather than filing a new one every
# Monday: unresolved drift is persistent by nature, and the previous
# unconditional `gh issue create` would stack a duplicate per run.
existing=$(gh issue list --state open --search "$title in:title" \
--json number,title -q "[.[] | select(.title == \"${title}\") | .number][0] // empty")
if [ -z "$existing" ]; then
gh issue create --title "$title" --body "$body"
else
gh issue comment "$existing" --body "$body"
echo "commented on #${existing}" >> "$GITHUB_STEP_SUMMARY"
fi
# Last, so the filing steps above still run. A PR touching semconv must
# not land with a stale mirror, and a scheduled run that found reviewable
# drift should read red on the Actions tab.
- name: Fail on unresolved drift
if: >-
steps.drift.outputs.drifted == 'true' &&
(github.event_name == 'pull_request' || steps.classify.outputs.mechanical == 'false')
run: |
echo "::error::tests/fixtures/semconv.json is behind rius-sdk-python."
echo "Regenerate it with: node scripts/gen-semconv-fixture.mjs"
exit 1