docs: require a CHANGELOG entry for user-facing changes #6
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 exempts from the | |
| # autogenerated-region edit guard and the changelog requirement. | |
| 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/python | |
| # This repo's docs folder (the source of truth). | |
| SOURCE_DOCS_PATH: docs | |
| # Stable branch on TARGET_REPO, force-pushed each run. | |
| SYNC_BRANCH: robot/docs-sync-clickhouse-connect | |
| # 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 absolute source path before we cd elsewhere. | |
| src="$(cd "$SOURCE_DOCS_PATH" && pwd)" | |
| 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 docs with this repo's docs. Wipe first so | |
| # deletions and renames on our side propagate. | |
| rm -rf "${workdir:?}/${TARGET_DOCS_PATH}" | |
| mkdir -p "${TARGET_DOCS_PATH}" | |
| cp -R "${src}/." "${TARGET_DOCS_PATH}/" | |
| 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="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. | |
| 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 |