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
212 changes: 212 additions & 0 deletions .github/workflows/docs_sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
name: 'Sync docs to ClickHouse/ClickHouse'

# Opens (or refreshes) a pull request on the aggregator docs repo
#
# It fires on three events:
# * a merged PR carrying the `sync-docs` label -> ship docs immediately,
# without waiting for a release
# * a published release -> ship docs on release, gated
# by RELEASE_SCOPE below (major-only by default);
# * manual dispatch -> force a sync.
#
# A single stable branch is force-pushed each run, so the bot keeps exactly one
# open PR that is always one commit off the target branch. The PR is labelled
# `pr-autogenerated-docs`, which the aggregator's docs check recognizes for the
# autogenerated-region edit guard. The generated body includes the changelog
# metadata required for the target repository's CI to initialize.

on:
pull_request_target:
types: [closed]
release:
types: [published]
workflow_dispatch:

# ---------------------------------------------------------------------------
# Configuration -- edit these to reuse this workflow in another client repo.
# ---------------------------------------------------------------------------
env:
# Repo that receives the docs PR, and the branch that PR targets.
TARGET_REPO: ClickHouse/ClickHouse
TARGET_BRANCH: master
# Path inside TARGET_REPO that mirrors this repo's docs. Wiped and replaced on
# every sync so deletions and renames propagate.
TARGET_DOCS_PATH: docs/integrations/language-clients/cpp
# This repo's docs file or folder (the source of truth).
SOURCE_DOCS_PATH: docs
# Stable branch on TARGET_REPO, force-pushed each run.
SYNC_BRANCH: robot/docs-sync-clickhouse-cpp
# Label the sync PR gets on TARGET_REPO.
PR_LABEL: pr-autogenerated-docs
# Label on a merged PR in THIS repo that triggers the expedited path.
SYNC_LABEL: sync-docs
# Which releases trigger a sync: major (X.0.0), minor (X.Y.0), or all.
RELEASE_SCOPE: all

# The cross-repo work is done with a GitHub App token (see the sync job); this
# workflow only needs to read its own repository.
permissions:
contents: read

jobs:
# Gate: decide whether this event should produce a sync, and why. Kept in its
# own job so the (large) checkout/clone in `sync` only runs when needed.
decide:
runs-on: ubuntu-latest
outputs:
run: ${{ steps.gate.outputs.run }}
reason: ${{ steps.gate.outputs.reason }}
steps:
- name: Decide whether to sync
id: gate
env:
EVENT_NAME: ${{ github.event_name }}
PR_MERGED: ${{ github.event.pull_request.merged }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
run: |
set -euo pipefail
run=false
reason=""
case "$EVENT_NAME" in
workflow_dispatch)
# Manual runs always sync -- the operator explicitly asked for it.
run=true
reason="manual dispatch"
;;
pull_request_target)
# Expedited path: a merged PR that carries the sync label.
if [[ "$PR_MERGED" == "true" ]] \
&& printf '%s' "$PR_LABELS" | jq -e --arg l "$SYNC_LABEL" 'index($l) != null' >/dev/null; then
run=true
reason="merged PR #${PR_NUMBER} labeled '${SYNC_LABEL}'"
fi
;;
release)
if [[ "$RELEASE_PRERELEASE" == "true" ]]; then
echo "Release ${RELEASE_TAG} is a prerelease; skipping."
elif [[ "${RELEASE_TAG#v}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
minor="${BASH_REMATCH[2]}"
patch="${BASH_REMATCH[3]}"
case "$RELEASE_SCOPE" in
all) run=true ;;
minor) [[ "$patch" == "0" ]] && run=true ;;
major|*) [[ "$minor" == "0" && "$patch" == "0" ]] && run=true ;;
esac
[[ "$run" == "true" ]] && reason="release ${RELEASE_TAG} (scope=${RELEASE_SCOPE})"
else
echo "Could not parse release tag '${RELEASE_TAG}'; skipping."
fi
;;
esac
echo "Decision: run=${run} reason='${reason}'"
{
echo "run=${run}"
echo "reason=${reason}"
} >> "$GITHUB_OUTPUT"

sync:
needs: decide
if: needs.decide.outputs.run == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout docs source
uses: actions/checkout@v5
with:
# On a release, take the docs as of the released tag; otherwise take
# the default checkout (post-merge default branch / dispatched ref).
ref: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }}

- name: Parse target repo
id: parse
run: |
set -euo pipefail
echo "owner=${TARGET_REPO%%/*}" >> "$GITHUB_OUTPUT"
echo "name=${TARGET_REPO##*/}" >> "$GITHUB_OUTPUT"

- name: Generate token for target repo
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.WORKFLOW_AUTH_PUBLIC_APP_ID }}
private-key: ${{ secrets.WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY }}
owner: ${{ steps.parse.outputs.owner }}
repositories: ${{ steps.parse.outputs.name }}

- name: Sync docs and open/refresh PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REASON: ${{ needs.decide.outputs.reason }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_REF: ${{ github.event_name == 'release' && github.event.release.tag_name || github.ref_name }}
SOURCE_SHA: ${{ github.sha }}
run: |
set -euo pipefail

# Resolve the absolute source path before we cd elsewhere.
src="$(realpath "$SOURCE_DOCS_PATH")"

workdir="$(mktemp -d)"
git clone --depth 1 --branch "$TARGET_BRANCH" \
"https://x-access-token:${GH_TOKEN}@github.com/${TARGET_REPO}.git" "$workdir"
cd "$workdir"

# Replace the target file or folder with this repo's docs. Wipe first
# so deletions and renames on our side propagate.
target="${workdir:?}/${TARGET_DOCS_PATH}"
rm -rf "$target"
mkdir -p "$(dirname "$target")"
if [[ -d "$src" ]]; then
mkdir -p "$target"
cp -R "${src}/." "${target}/"
else
cp "$src" "$target"
fi

git config user.name "clickhouse-docs-bot"
git config user.email "clickhouse-docs-bot@users.noreply.github.com"

# Build the single-commit-off-target branch fresh from the target
# branch so the PR is always exactly one commit ahead.
git checkout -B "$SYNC_BRANCH"
git add -A -- "$TARGET_DOCS_PATH"
if git diff --cached --quiet; then
echo "No docs changes to sync; nothing to do."
exit 0
fi

git commit -m "Sync ${SOURCE_REPO} docs (${REASON})"
git push --force origin "$SYNC_BRANCH"

title="Docs: Sync ${SOURCE_REPO} docs"
body=$(cat <<EOF
Automated one-way docs sync from [\`${SOURCE_REPO}\`](https://github.com/${SOURCE_REPO}).

- **Source ref:** \`${SOURCE_REF}\` (\`${SOURCE_SHA}\`)
- **Trigger:** ${REASON}
- **Replaces:** \`${TARGET_DOCS_PATH}\`

This PR is generated by the \`Sync docs to ClickHouse/ClickHouse\` workflow in \`${SOURCE_REPO}\`.
The source repo is the source of truth for these docs; edit them there, not here.

### Changelog category (leave one):
- Documentation (changelog entry is not required)

### Changelog entry (a [user-readable short description](https://github.com/ClickHouse/ClickHouse/blob/master/docs/changelog_entry_guidelines.md) of the changes that goes into CHANGELOG.md):
Sync language client documentation from \`${SOURCE_REPO}\`.
EOF
)

existing="$(gh pr list --repo "$TARGET_REPO" --head "$SYNC_BRANCH" \
--state open --json number --jq '.[0].number // empty')"
if [[ -n "$existing" ]]; then
echo "PR #${existing} already open; force-push refreshed it."
gh pr edit "$existing" --repo "$TARGET_REPO" \
--title "$title" --body "$body" --add-label "$PR_LABEL"
else
gh pr create --repo "$TARGET_REPO" \
--base "$TARGET_BRANCH" --head "$SYNC_BRANCH" \
--title "$title" --body "$body" --label "$PR_LABEL"
fi
50 changes: 50 additions & 0 deletions .github/workflows/docs_verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: 'Verify Docs'

on:
pull_request:
branches:
- master
paths:
- 'docs/**'
- '.github/workflows/docs_verify.yml'
push:
branches:
- master
paths:
- 'docs/**'
- '.github/workflows/docs_verify.yml'

jobs:
verify:
runs-on: ubuntu-latest
name: Mintlify checks
# Run inside the docs image so every dependency (git, python3, mint, node)
# is present -- nothing is installed, and this repo vendors no scripts.
container:
image: clickhouse/docs-builder
steps:
- name: Checkout docs source
uses: actions/checkout@v5

# Fetch the check driver from ClickHouse/ClickHouse rather than vendoring
# it. The driver shallow/sparse-clones the aggregator docs plus its CI
# scripts (ci/jobs/scripts/docs), so any check scripts come along for free.
- name: Fetch the docs check driver
run: |
curl -fsSL -o /tmp/mintlify_docs_check.py \
https://raw.githubusercontent.com/ClickHouse/ClickHouse/master/ci/jobs/scripts/docs/mintlify_docs_check.py

# The driver clones the aggregator, replaces the C++ client page with this
# repo's canonical docs (wiping it first), and
# runs the checks from the docs root. --scoped swaps the full site-wide
# `mint validate` (~13 min) for a scoped validate of just the replaced
# slice (docs.json schema, navigation, MDX parse, snippet imports);
# link integrity is still checked site-wide by the lychee check.
# It exits non-zero if any check fails, so a green step means all passed.
- name: Run Mintlify docs checks
run: |
python3 /tmp/mintlify_docs_check.py \
--repo https://github.com/ClickHouse/ClickHouse.git \
--ref master \
--replace docs:integrations/language-clients/cpp \
--scoped
Loading
Loading