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
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/java
# This repo's docs file or folder (the source of truth).
SOURCE_DOCS_PATH: docs/clickhouse-docs
# Stable branch on TARGET_REPO, force-pushed each run.
SYNC_BRANCH: robot/docs-sync-clickhouse-java
# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Release sync skips prereleases

Medium Severity

The new sync workflow skips published releases when github.event.release.prerelease is true, but the repository’s existing Release workflow always creates GitHub releases with --prerelease. The release trigger path therefore never runs after a normal release; docs only sync via sync-docs merges or manual dispatch unless someone later clears the prerelease flag by hand.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 592e8d1. Configure here.

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 || '' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fork merge checkout fails

High Severity

The sync job leaves actions/checkout@v5 on its default ref for merged pull_request_target runs. Since the July 2026 checkout backport, that default often resolves to the fork merge commit and the step refuses to check out, so the expedited sync-docs path can fail after external PRs merge even though the gate passed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 592e8d1. Configure here.


- 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
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Indented heredoc breaks body

Medium Severity

The aggregator PR body is built with a plain heredoc whose lines are indented to match the YAML block. Bash keeps those leading spaces, so changelog headings and bullets are not at column zero and downstream ClickHouse/ClickHouse PR checks may not recognize the required template.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 592e8d1. Configure here.


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:
- main
paths:
- 'docs/clickhouse-docs/**'
- '.github/workflows/docs_verify.yml'
push:
branches:
- main
paths:
- 'docs/clickhouse-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 Java client folder 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/clickhouse-docs:integrations/language-clients/java \
--scoped
Loading
Loading