From a06a6d247bf8fb65781f266dc1aef36b4578c61c Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Sat, 12 Sep 2026 05:39:29 +0200 Subject: [PATCH 1/8] Add reusable remote docs preview dispatcher --- .github/workflows/remote-docs-preview.yml | 118 ++++++++++++++++++++++ .github/zizmor.yml | 1 + README.md | 28 +++++ examples/caller-remote-docs-preview.yml | 32 ++++++ 4 files changed, 179 insertions(+) create mode 100644 .github/workflows/remote-docs-preview.yml create mode 100644 examples/caller-remote-docs-preview.yml diff --git a/.github/workflows/remote-docs-preview.yml b/.github/workflows/remote-docs-preview.yml new file mode 100644 index 0000000..04c1535 --- /dev/null +++ b/.github/workflows/remote-docs-preview.yml @@ -0,0 +1,118 @@ +name: Dispatch remote documentation preview + +# A remote documentation repository calls this workflow manually after a +# maintainer has reviewed the pull request. The workflow resolves and pins the +# approved PR revision, then dispatches the credentialed build in the central +# docs repository. It never checks out or executes pull-request content and it +# never receives Vercel credentials. + +on: + workflow_call: + inputs: + remote_name: + description: Source name registered in the central docs remotes.json + required: true + type: string + pull_request_number: + description: Open pull request to preview + required: true + type: number + secrets: + WORKFLOW_AUTH_PUBLIC_APP_ID: + required: true + WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY: + required: true + +permissions: {} + +jobs: + dispatch: + name: Request docs preview + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + pull-requests: read + concurrency: + group: remote-docs-preview-dispatch-${{ github.repository }}-${{ inputs.pull_request_number }} + cancel-in-progress: true + steps: + - name: Validate the request + id: request + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + EVENT_NAME: ${{ github.event_name }} + PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} + REMOTE_NAME: ${{ inputs.remote_name }} + SOURCE_REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + if [[ "$EVENT_NAME" != "workflow_dispatch" ]]; then + echo "::error::Remote documentation previews must be requested through a manual workflow_dispatch event." + exit 1 + fi + if [[ "$GITHUB_REF_NAME" != "$DEFAULT_BRANCH" ]]; then + echo "::error::Run the caller workflow from the repository's default branch." + exit 1 + fi + if [[ ! "$REMOTE_NAME" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then + echo "::error::remote_name must contain only lowercase letters, numbers, and hyphens." + exit 1 + fi + if [[ ! "$PULL_REQUEST_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::pull_request_number must be a positive integer." + exit 1 + fi + + pull_request="$( + gh api "repos/$SOURCE_REPOSITORY/pulls/$PULL_REQUEST_NUMBER" + )" + if [[ "$(jq -r .state <<< "$pull_request")" != "open" ]]; then + echo "::error::Pull request $SOURCE_REPOSITORY#$PULL_REQUEST_NUMBER is not open." + exit 1 + fi + + head_sha="$(jq -er .head.sha <<< "$pull_request")" + if [[ ! "$head_sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::GitHub returned an invalid pull-request head SHA." + exit 1 + fi + + echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" + + - name: Mint a docs-repository dispatch token + id: docs-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.WORKFLOW_AUTH_PUBLIC_APP_ID }} + private-key: ${{ secrets.WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY }} + owner: ClickHouse + repositories: mintlify-docs-dev + permission-actions: write + permission-contents: read + + - name: Dispatch the central preview build + env: + APPROVED_HEAD_SHA: ${{ steps.request.outputs.head_sha }} + DOCS_REPOSITORY: ClickHouse/mintlify-docs-dev + GH_TOKEN: ${{ steps.docs-token.outputs.token }} + PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} + REMOTE_NAME: ${{ inputs.remote_name }} + SOURCE_REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + + gh workflow run remote-docs-preview.yml \ + --repo "$DOCS_REPOSITORY" \ + --ref main \ + --field remote_name="$REMOTE_NAME" \ + --field source_repository="$SOURCE_REPOSITORY" \ + --field pull_request_number="$PULL_REQUEST_NUMBER" \ + --field approved_head_sha="$APPROVED_HEAD_SHA" + + { + echo "### Documentation preview requested" + echo + echo "The central docs workflow will build \`$REMOTE_NAME\` from \`$SOURCE_REPOSITORY@$APPROVED_HEAD_SHA\` and add the preview URL to pull request #$PULL_REQUEST_NUMBER." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 5ab44a6..3ec61cf 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -22,3 +22,4 @@ rules: # you add an example caller. - caller-claude-pr-triage.yml - caller-claude-docs-drift.yml + - caller-remote-docs-preview.yml diff --git a/README.md b/README.md index b1f8105..e49dd7f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,34 @@ so the logic lives in one place and per-repo specifics are passed as inputs. ## Workflows +### `remote-docs-preview.yml` - Request a remote documentation preview + +Starts a scoped documentation preview for a pull request in a repository +registered by `ClickHouse/mintlify-docs-dev/remotes.json`. A maintainer invokes +the caller manually from the remote repository's default branch. The shared +workflow validates the open pull request, pins its exact head SHA, and uses the +Workflow Authentication GitHub App to dispatch the central deployment workflow +in `ClickHouse/mintlify-docs-dev`. + +The remote repository never receives Vercel credentials and the workflow never +checks out or executes pull-request content. The central docs workflow builds +trusted Nimbus code, fetches only the selected remote revision through Vercel +Connect, and comments the resulting URL on the source pull request. + +| Input | Required | Purpose | +|---|---|---| +| `remote_name` | yes | Source name in the central `remotes.json` registry. | +| `pull_request_number` | yes | Open pull request whose exact head SHA should be previewed. | + +Required secrets are `WORKFLOW_AUTH_PUBLIC_APP_ID` and +`WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY`. The GitHub App must be installed on both the +source repository and `ClickHouse/mintlify-docs-dev`, with pull-request read +access on the source and Actions write access on the docs repository. + +See [`examples/caller-remote-docs-preview.yml`](examples/caller-remote-docs-preview.yml) +for a copy-paste manual caller. Set `remote_name` to the source's registered +name; no Vercel secret is needed in the remote repository. + ### `claude-docs-drift.yml` - Dispatch centralized docs drift checks Sends a source pull request to the central docs-drift worker in diff --git a/examples/caller-remote-docs-preview.yml b/examples/caller-remote-docs-preview.yml new file mode 100644 index 0000000..73dab07 --- /dev/null +++ b/examples/caller-remote-docs-preview.yml @@ -0,0 +1,32 @@ +# Install as .github/workflows/docs-preview.yml in a repository registered as a +# remote documentation source in ClickHouse/mintlify-docs-dev/remotes.json. +# +# A maintainer starts the workflow manually from the repository's default +# branch and supplies the open pull request number. Pull-request code is not +# checked out or executed in Actions. The central docs repository performs the +# Vercel build and comments its URL on the source pull request. +name: Preview documentation + +run-name: Preview documentation for PR #${{ inputs.pull_request_number }} + +on: + workflow_dispatch: + inputs: + pull_request_number: + description: Pull request number to preview + required: true + type: number + +permissions: + pull-requests: read + +jobs: + preview: + uses: ClickHouse/integrations-shared-workflows/.github/workflows/remote-docs-preview.yml@main + with: + # This must match the source's name in the central remotes.json registry. + remote_name: clickhouse-private + pull_request_number: ${{ inputs.pull_request_number }} + secrets: + WORKFLOW_AUTH_PUBLIC_APP_ID: ${{ secrets.WORKFLOW_AUTH_PUBLIC_APP_ID }} + WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY: ${{ secrets.WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY }} From 3089c6338e59ad663265816b8b84329b4863924f Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:11:16 +0200 Subject: [PATCH 2/8] Trigger remote docs previews directly on Vercel --- .github/workflows/remote-docs-preview.yml | 358 +++++++++++++++++++--- README.md | 28 +- examples/caller-remote-docs-preview.yml | 8 +- 3 files changed, 331 insertions(+), 63 deletions(-) diff --git a/.github/workflows/remote-docs-preview.yml b/.github/workflows/remote-docs-preview.yml index 04c1535..9a7a553 100644 --- a/.github/workflows/remote-docs-preview.yml +++ b/.github/workflows/remote-docs-preview.yml @@ -1,10 +1,9 @@ -name: Dispatch remote documentation preview +name: Deploy remote documentation preview -# A remote documentation repository calls this workflow manually after a -# maintainer has reviewed the pull request. The workflow resolves and pins the -# approved PR revision, then dispatches the credentialed build in the central -# docs repository. It never checks out or executes pull-request content and it -# never receives Vercel credentials. +# A maintainer starts the caller manually from the source repository's default +# branch. This workflow resolves the approved PR revision and creates a Vercel +# deployment of trusted Nimbus main. The deployment—not Actions—uses Vercel +# Connect to obtain a short-lived token for the selected remote repository. on: workflow_call: @@ -18,33 +17,43 @@ on: required: true type: number secrets: - WORKFLOW_AUTH_PUBLIC_APP_ID: + VERCEL_TOKEN: required: true - WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY: + VERCEL_ORG_ID: required: true + VERCEL_PROJECT_ID: + required: true + outputs: + preview_url: + description: URL of the completed Vercel preview + value: ${{ jobs.preview.outputs.preview_url }} permissions: {} jobs: - dispatch: - name: Request docs preview + preview: + name: Build docs preview on Vercel runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 60 permissions: - pull-requests: read + contents: read + pull-requests: write concurrency: - group: remote-docs-preview-dispatch-${{ github.repository }}-${{ inputs.pull_request_number }} + group: remote-docs-preview-${{ github.repository }}-${{ inputs.pull_request_number }} cancel-in-progress: true + outputs: + preview_url: ${{ steps.deploy.outputs.preview_url }} steps: - - name: Validate the request + - name: Resolve the approved revisions id: request env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + DOCS_REPOSITORY: ClickHouse/mintlify-docs-dev EVENT_NAME: ${{ github.event_name }} + GH_TOKEN: ${{ github.token }} PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} REMOTE_NAME: ${{ inputs.remote_name }} SOURCE_REPOSITORY: ${{ github.repository }} - GH_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -65,54 +74,307 @@ jobs: exit 1 fi - pull_request="$( - gh api "repos/$SOURCE_REPOSITORY/pulls/$PULL_REQUEST_NUMBER" - )" + pull_request="$(gh api "repos/$SOURCE_REPOSITORY/pulls/$PULL_REQUEST_NUMBER")" if [[ "$(jq -r .state <<< "$pull_request")" != "open" ]]; then echo "::error::Pull request $SOURCE_REPOSITORY#$PULL_REQUEST_NUMBER is not open." exit 1 fi head_sha="$(jq -er .head.sha <<< "$pull_request")" + head_repository="$(jq -er .head.repo.full_name <<< "$pull_request")" + site_sha="$(gh api "repos/$DOCS_REPOSITORY/commits/main" --jq .sha)" if [[ ! "$head_sha" =~ ^[0-9a-f]{40}$ ]]; then echo "::error::GitHub returned an invalid pull-request head SHA." exit 1 fi + if [[ ! "$head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo "::error::GitHub returned an invalid pull-request head repository." + exit 1 + fi + if [[ ! "$site_sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::GitHub returned an invalid central docs SHA." + exit 1 + fi - echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" - - - name: Mint a docs-repository dispatch token - id: docs-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - app-id: ${{ secrets.WORKFLOW_AUTH_PUBLIC_APP_ID }} - private-key: ${{ secrets.WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY }} - owner: ClickHouse - repositories: mintlify-docs-dev - permission-actions: write - permission-contents: read + { + echo "head_sha=$head_sha" + echo "head_repository=$head_repository" + echo "site_sha=$site_sha" + } >> "$GITHUB_OUTPUT" - - name: Dispatch the central preview build + # The payload contains no GitHub credential. Vercel fetches the trusted + # Nimbus revision through the project's Git connection. During that + # build, @vercel/connect exchanges VERCEL_OIDC_TOKEN for a short-lived, + # contents:read token scoped to the selected source repository. + - name: Trigger and wait for the Vercel Connect preview + id: deploy env: - APPROVED_HEAD_SHA: ${{ steps.request.outputs.head_sha }} - DOCS_REPOSITORY: ClickHouse/mintlify-docs-dev - GH_TOKEN: ${{ steps.docs-token.outputs.token }} + DOCS_REMOTE_NAME: ${{ inputs.remote_name }} + DOCS_REMOTE_REF: ${{ steps.request.outputs.head_sha }} + DOCS_REMOTE_REPOSITORY: ${{ github.repository }} + DOCS_REMOTE_SOURCE_REPOSITORY: ${{ steps.request.outputs.head_repository }} PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} - REMOTE_NAME: ${{ inputs.remote_name }} - SOURCE_REPOSITORY: ${{ github.repository }} + SITE_SHA: ${{ steps.request.outputs.site_sha }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: | set -euo pipefail - gh workflow run remote-docs-preview.yml \ - --repo "$DOCS_REPOSITORY" \ - --ref main \ - --field remote_name="$REMOTE_NAME" \ - --field source_repository="$SOURCE_REPOSITORY" \ - --field pull_request_number="$PULL_REQUEST_NUMBER" \ - --field approved_head_sha="$APPROVED_HEAD_SHA" + deployment_id="" + deployment_finished=false - { - echo "### Documentation preview requested" - echo - echo "The central docs workflow will build \`$REMOTE_NAME\` from \`$SOURCE_REPOSITORY@$APPROVED_HEAD_SHA\` and add the preview URL to pull request #$PULL_REQUEST_NUMBER." - } >> "$GITHUB_STEP_SUMMARY" + cancel_vercel_deployment() { + local deployment_id_to_cancel="$1" + local deployment_label="$2" + local cancel_response + local cancel_exit_code + local deployment_response + local ready_state + + if cancel_response="$( + curl --silent --show-error --fail-with-body \ + --request PATCH \ + --header "Authorization: Bearer $VERCEL_TOKEN" \ + "https://api.vercel.com/v12/deployments/$deployment_id_to_cancel/cancel?teamId=$VERCEL_ORG_ID" + )"; then + echo "Canceled $deployment_label Vercel deployment $deployment_id_to_cancel." + return 0 + else + cancel_exit_code=$? + fi + + if deployment_response="$( + curl --silent --show-error --fail-with-body \ + --header "Authorization: Bearer $VERCEL_TOKEN" \ + "https://api.vercel.com/v13/deployments/$deployment_id_to_cancel?teamId=$VERCEL_ORG_ID" + )"; then + ready_state="$(jq -er .readyState <<< "$deployment_response")" + case "$ready_state" in + READY|ERROR|CANCELED|DELETED|BLOCKED) + echo "$deployment_label Vercel deployment $deployment_id_to_cancel already reached $ready_state." + return 0 + ;; + esac + fi + + printf '%s\n' "$cancel_response" >&2 + echo "Unable to cancel $deployment_label Vercel deployment $deployment_id_to_cancel." >&2 + return "$cancel_exit_code" + } + + cancel_superseded_deployments() { + local ready_state + local until="" + local list_response + local next + local superseded_id + local -a request_args + local -a deployment_ids + + for ready_state in QUEUED INITIALIZING BUILDING; do + until="" + while true; do + request_args=( + --silent + --show-error + --fail-with-body + --get + --header "Authorization: Bearer $VERCEL_TOKEN" + --data-urlencode "projectId=$VERCEL_PROJECT_ID" + --data-urlencode "state=$ready_state" + --data-urlencode "limit=100" + --data-urlencode "teamId=$VERCEL_ORG_ID" + ) + if [[ -n "$until" ]]; then + request_args+=(--data-urlencode "until=$until") + fi + + if ! list_response="$( + curl "${request_args[@]}" "https://api.vercel.com/v7/deployments" + )"; then + printf '%s\n' "$list_response" >&2 + echo "Unable to list source-preview Vercel deployments." >&2 + return 1 + fi + + mapfile -t deployment_ids < <( + jq -r \ + --arg pull_request "$PULL_REQUEST_NUMBER" \ + --arg source_repository "$DOCS_REMOTE_REPOSITORY" \ + '.deployments[] + | select((.meta.buildScope? // "") == "source-preview") + | select((.meta.sourceRepository? // "") == $source_repository) + | select((.meta.pullRequest? // "") == $pull_request) + | (.uid // .id)' \ + <<< "$list_response" + ) + for superseded_id in "${deployment_ids[@]}"; do + cancel_vercel_deployment "$superseded_id" "superseded source-preview" || return 1 + done + + next="$(jq -er '.pagination.next // ""' <<< "$list_response")" + if [[ -z "$next" ]]; then + break + fi + if [[ "$next" == "$until" ]]; then + echo "Vercel returned a repeated pagination cursor while listing source-preview deployments." >&2 + return 1 + fi + until="$next" + done + done + } + + finalize_deployment() { + local exit_code=$? + trap - EXIT INT TERM + if [[ "$deployment_finished" != true && -n "$deployment_id" ]]; then + if ! cancel_vercel_deployment "$deployment_id" "source-preview"; then + echo "::warning::Unable to cancel the interrupted source-preview deployment." + if [[ "$exit_code" -eq 0 ]]; then + exit_code=1 + fi + fi + fi + exit "$exit_code" + } + + cancel_superseded_deployments + + trap finalize_deployment EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + + if ! project_response="$( + curl --silent --show-error --fail-with-body \ + --header "Authorization: Bearer $VERCEL_TOKEN" \ + "https://api.vercel.com/v9/projects/$VERCEL_PROJECT_ID?teamId=$VERCEL_ORG_ID" + )"; then + printf '%s\n' "$project_response" >&2 + exit 1 + fi + project_name="$(jq -er .name <<< "$project_response")" + + payload="$( + jq --null-input \ + --arg approved_sha "$DOCS_REMOTE_REF" \ + --arg project_id "$VERCEL_PROJECT_ID" \ + --arg project_name "$project_name" \ + --arg pull_request "$PULL_REQUEST_NUMBER" \ + --arg remote_name "$DOCS_REMOTE_NAME" \ + --arg remote_repository "$DOCS_REMOTE_REPOSITORY" \ + --arg source_repository "$DOCS_REMOTE_SOURCE_REPOSITORY" \ + --arg site_sha "$SITE_SHA" \ + '{ + name: $project_name, + project: $project_id, + customEnvironmentSlugOrId: "connect-preview", + gitSource: { + type: "github", + org: "ClickHouse", + repo: "mintlify-docs-dev", + ref: "main", + sha: $site_sha + }, + build: {env: { + DOCS_DEPLOY_TARGET: "english", + DOCS_LOCALES: "none", + DOCS_AVAILABLE_LOCALES: "all", + DOCS_REMOTES: "all", + DOCS_REMOTE_NAME: $remote_name, + DOCS_REMOTE_REPOSITORY: $remote_repository, + DOCS_REMOTE_SOURCE_REPOSITORY: $source_repository, + DOCS_REMOTE_REF: $approved_sha + }}, + meta: { + buildScope: "source-preview", + sourceRepository: $remote_repository, + sourceHeadRepository: $source_repository, + pullRequest: $pull_request, + approvedSha: $approved_sha, + siteSha: $site_sha + } + }' + )" + + if ! deployment_response="$( + curl --silent --show-error --fail-with-body \ + --request POST \ + --header "Authorization: Bearer $VERCEL_TOKEN" \ + --header "Content-Type: application/json" \ + --data "$payload" \ + "https://api.vercel.com/v13/deployments?forceNew=1&teamId=$VERCEL_ORG_ID" + )"; then + printf '%s\n' "$deployment_response" >&2 + exit 1 + fi + + deployment_id="$(jq -er .id <<< "$deployment_response")" + deployment_url="$(jq -er .url <<< "$deployment_response")" + preview_url="https://${deployment_url#https://}" + echo "preview_url=$preview_url" >> "$GITHUB_OUTPUT" + echo "Vercel deployment: $preview_url" + + for _ in {1..360}; do + if ! deployment_response="$( + curl --silent --show-error --fail-with-body \ + --header "Authorization: Bearer $VERCEL_TOKEN" \ + "https://api.vercel.com/v13/deployments/$deployment_id?teamId=$VERCEL_ORG_ID" + )"; then + printf '%s\n' "$deployment_response" >&2 + exit 1 + fi + + ready_state="$(jq -er .readyState <<< "$deployment_response")" + case "$ready_state" in + READY) + break + ;; + ERROR|CANCELED|DELETED|BLOCKED) + jq -r '(.errorCode // "deployment_failed") + ": " + (.errorMessage // "Vercel deployment did not complete")' \ + <<< "$deployment_response" >&2 + exit 1 + ;; + QUEUED|INITIALIZING|BUILDING) + sleep 10 + ;; + *) + echo "Unexpected Vercel deployment state: $ready_state" >&2 + exit 1 + ;; + esac + done + + if [[ "$ready_state" != "READY" ]]; then + echo "Timed out waiting for Vercel deployment $deployment_id." >&2 + exit 1 + fi + if [[ ! "$preview_url" =~ ^https://[^[:space:]]+$ ]]; then + echo "Vercel returned an invalid preview URL." >&2 + exit 1 + fi + deployment_finished=true + + - name: Add the preview link to the pull request + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }} + PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} + run: | + set -euo pipefail + + marker="" + docs_url="${PREVIEW_URL%/}/docs" + body="$(printf '%s\n\nDocumentation preview: %s' "$marker" "$docs_url")" + comment_id="$( + gh api --paginate "repos/$GH_REPO/issues/$PULL_REQUEST_NUMBER/comments" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"$marker\")) | .id" \ + | tail -n 1 + )" + if [[ -n "$comment_id" ]]; then + gh api --method PATCH "repos/$GH_REPO/issues/comments/$comment_id" -f body="$body" >/dev/null + else + gh pr comment "$PULL_REQUEST_NUMBER" --body "$body" + fi diff --git a/README.md b/README.md index e49dd7f..5885201 100644 --- a/README.md +++ b/README.md @@ -11,28 +11,32 @@ so the logic lives in one place and per-repo specifics are passed as inputs. Starts a scoped documentation preview for a pull request in a repository registered by `ClickHouse/mintlify-docs-dev/remotes.json`. A maintainer invokes the caller manually from the remote repository's default branch. The shared -workflow validates the open pull request, pins its exact head SHA, and uses the -Workflow Authentication GitHub App to dispatch the central deployment workflow -in `ClickHouse/mintlify-docs-dev`. +workflow validates the open pull request, pins its exact head SHA, and directly +creates a Vercel deployment of the current trusted Nimbus `main` revision in the +`connect-preview` Custom Environment. -The remote repository never receives Vercel credentials and the workflow never -checks out or executes pull-request content. The central docs workflow builds -trusted Nimbus code, fetches only the selected remote revision through Vercel -Connect, and comments the resulting URL on the source pull request. +The workflow never checks out or executes pull-request content and never passes +a GitHub credential to Vercel. Inside the deployment, Vercel Connect exchanges +the deployment's OIDC identity for a short-lived `contents:read` token scoped to +the selected repository. Nimbus fetches the approved revision before it removes +the token and begins processing Markdown or MDX. The shared workflow waits for +the deployment and comments the resulting URL on the source pull request. | Input | Required | Purpose | |---|---|---| | `remote_name` | yes | Source name in the central `remotes.json` registry. | | `pull_request_number` | yes | Open pull request whose exact head SHA should be previewed. | -Required secrets are `WORKFLOW_AUTH_PUBLIC_APP_ID` and -`WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY`. The GitHub App must be installed on both the -source repository and `ClickHouse/mintlify-docs-dev`, with pull-request read -access on the source and Actions write access on the docs repository. +Required secrets are `VERCEL_TOKEN`, `VERCEL_ORG_ID`, and +`VERCEL_PROJECT_ID`. Define them once as organization Actions secrets and grant +them to the registered source repositories; callers can then pass the three +secrets explicitly without duplicating their values per repository. The Vercel +project must provide `DOCS_GITHUB_CONNECTOR` in its `connect-preview` Custom +Environment and allow that environment to use the Vercel Connect GitHub app. See [`examples/caller-remote-docs-preview.yml`](examples/caller-remote-docs-preview.yml) for a copy-paste manual caller. Set `remote_name` to the source's registered -name; no Vercel secret is needed in the remote repository. +name. ### `claude-docs-drift.yml` - Dispatch centralized docs drift checks diff --git a/examples/caller-remote-docs-preview.yml b/examples/caller-remote-docs-preview.yml index 73dab07..064fd22 100644 --- a/examples/caller-remote-docs-preview.yml +++ b/examples/caller-remote-docs-preview.yml @@ -18,7 +18,8 @@ on: type: number permissions: - pull-requests: read + contents: read + pull-requests: write jobs: preview: @@ -28,5 +29,6 @@ jobs: remote_name: clickhouse-private pull_request_number: ${{ inputs.pull_request_number }} secrets: - WORKFLOW_AUTH_PUBLIC_APP_ID: ${{ secrets.WORKFLOW_AUTH_PUBLIC_APP_ID }} - WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY: ${{ secrets.WORKFLOW_AUTH_PUBLIC_PRIVATE_KEY }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} From f4c1079a6f044bbc0f16b20f690e86242a7fce61 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:16:51 +0200 Subject: [PATCH 3/8] Trigger remote previews from approval label --- .github/workflows/remote-docs-preview.yml | 25 +++++++++++++++-------- .github/zizmor.yml | 8 ++++++++ README.md | 11 ++++++---- examples/caller-remote-docs-preview.yml | 21 ++++++++----------- 4 files changed, 41 insertions(+), 24 deletions(-) diff --git a/.github/workflows/remote-docs-preview.yml b/.github/workflows/remote-docs-preview.yml index 9a7a553..042768d 100644 --- a/.github/workflows/remote-docs-preview.yml +++ b/.github/workflows/remote-docs-preview.yml @@ -1,9 +1,10 @@ name: Deploy remote documentation preview -# A maintainer starts the caller manually from the source repository's default -# branch. This workflow resolves the approved PR revision and creates a Vercel -# deployment of trusted Nimbus main. The deployment—not Actions—uses Vercel -# Connect to obtain a short-lived token for the selected remote repository. +# A maintainer approves a source pull request by adding the `docs-preview` +# label. The trusted pull_request_target caller invokes this workflow, which +# resolves the approved PR revision and creates a Vercel deployment of trusted +# Nimbus main. The deployment—not Actions—uses Vercel Connect to obtain a +# short-lived token for the selected remote repository. on: workflow_call: @@ -48,8 +49,12 @@ jobs: id: request env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + EVENT_ACTION: ${{ github.event.action }} + EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }} + EVENT_LABEL: ${{ github.event.label.name }} DOCS_REPOSITORY: ClickHouse/mintlify-docs-dev EVENT_NAME: ${{ github.event_name }} + EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_TOKEN: ${{ github.token }} PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} REMOTE_NAME: ${{ inputs.remote_name }} @@ -57,12 +62,12 @@ jobs: run: | set -euo pipefail - if [[ "$EVENT_NAME" != "workflow_dispatch" ]]; then - echo "::error::Remote documentation previews must be requested through a manual workflow_dispatch event." + if [[ "$EVENT_NAME" != "pull_request_target" || "$EVENT_ACTION" != "labeled" || "$EVENT_LABEL" != "docs-preview" ]]; then + echo "::error::Remote documentation previews require a pull_request_target labeled event for docs-preview." exit 1 fi - if [[ "$GITHUB_REF_NAME" != "$DEFAULT_BRANCH" ]]; then - echo "::error::Run the caller workflow from the repository's default branch." + if [[ "$EVENT_BASE_REF" != "$DEFAULT_BRANCH" ]]; then + echo "::error::The pull request must target the repository's default branch." exit 1 fi if [[ ! "$REMOTE_NAME" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then @@ -73,6 +78,10 @@ jobs: echo "::error::pull_request_number must be a positive integer." exit 1 fi + if [[ "$PULL_REQUEST_NUMBER" != "$EVENT_PULL_REQUEST_NUMBER" ]]; then + echo "::error::pull_request_number must match the labeled pull request." + exit 1 + fi pull_request="$(gh api "repos/$SOURCE_REPOSITORY/pulls/$PULL_REQUEST_NUMBER")" if [[ "$(jq -r .state <<< "$pull_request")" != "open" ]]; then diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 3ec61cf..f26bf35 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -12,6 +12,14 @@ # with refs that are stale or unpinned. rules: + dangerous-triggers: + ignore: + # This caller intentionally uses pull_request_target so a maintainer can + # authorize a fork preview by adding `docs-preview`. It accepts only the + # labeled event, checks the exact label again in the reusable workflow, + # and never checks out or executes pull-request content. Vercel Connect + # fetches the approved SHA and credentials are removed before MDX runs. + - caller-remote-docs-preview.yml unpinned-uses: ignore: # Callers are told to reference these reusable workflows at `@main` — that diff --git a/README.md b/README.md index 5885201..39a4950 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ so the logic lives in one place and per-repo specifics are passed as inputs. Starts a scoped documentation preview for a pull request in a repository registered by `ClickHouse/mintlify-docs-dev/remotes.json`. A maintainer invokes -the caller manually from the remote repository's default branch. The shared -workflow validates the open pull request, pins its exact head SHA, and directly +the caller by adding the `docs-preview` label to a pull request targeting the +remote repository's default branch. The shared workflow validates the trusted +label event, pins the pull request's exact head SHA, and directly creates a Vercel deployment of the current trusted Nimbus `main` revision in the `connect-preview` Custom Environment. @@ -35,8 +36,10 @@ project must provide `DOCS_GITHUB_CONNECTOR` in its `connect-preview` Custom Environment and allow that environment to use the Vercel Connect GitHub app. See [`examples/caller-remote-docs-preview.yml`](examples/caller-remote-docs-preview.yml) -for a copy-paste manual caller. Set `remote_name` to the source's registered -name. +for a copy-paste `pull_request_target` caller. Set `remote_name` to the source's +registered name. The caller deliberately listens only for `labeled` events and +the shared workflow independently verifies the `docs-preview` label, action, +pull request number, and default target branch. ### `claude-docs-drift.yml` - Dispatch centralized docs drift checks diff --git a/examples/caller-remote-docs-preview.yml b/examples/caller-remote-docs-preview.yml index 064fd22..3e15cc0 100644 --- a/examples/caller-remote-docs-preview.yml +++ b/examples/caller-remote-docs-preview.yml @@ -1,21 +1,17 @@ # Install as .github/workflows/docs-preview.yml in a repository registered as a # remote documentation source in ClickHouse/mintlify-docs-dev/remotes.json. # -# A maintainer starts the workflow manually from the repository's default -# branch and supplies the open pull request number. Pull-request code is not -# checked out or executed in Actions. The central docs repository performs the -# Vercel build and comments its URL on the source pull request. +# A maintainer approves a pull request by adding the `docs-preview` label. This +# uses pull_request_target so organization secrets are available for fork PRs, +# but neither this caller nor the shared workflow checks out or executes PR +# code. Vercel Connect fetches the approved remote revision inside the build. name: Preview documentation -run-name: Preview documentation for PR #${{ inputs.pull_request_number }} +run-name: Preview documentation for PR #${{ github.event.pull_request.number }} on: - workflow_dispatch: - inputs: - pull_request_number: - description: Pull request number to preview - required: true - type: number + pull_request_target: + types: [labeled] permissions: contents: read @@ -23,11 +19,12 @@ permissions: jobs: preview: + if: github.event.label.name == 'docs-preview' uses: ClickHouse/integrations-shared-workflows/.github/workflows/remote-docs-preview.yml@main with: # This must match the source's name in the central remotes.json registry. remote_name: clickhouse-private - pull_request_number: ${{ inputs.pull_request_number }} + pull_request_number: ${{ github.event.pull_request.number }} secrets: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} From 9f33f5c3daab34437f85c96ca7000a611ffdd87d Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Sat, 12 Sep 2026 07:03:05 +0200 Subject: [PATCH 4/8] Make docs preview path filters configurable --- README.md | 10 +++++++--- examples/caller-remote-docs-preview.yml | 6 ++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 39a4950..369ca90 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,13 @@ Environment and allow that environment to use the Vercel Connect GitHub app. See [`examples/caller-remote-docs-preview.yml`](examples/caller-remote-docs-preview.yml) for a copy-paste `pull_request_target` caller. Set `remote_name` to the source's -registered name. The caller deliberately listens only for `labeled` events and -the shared workflow independently verifies the `docs-preview` label, action, -pull request number, and default target branch. +registered name and configure the caller's native `paths` filter for the files +that should be eligible, such as `docs/**`. Remove `paths` when every pull +request should be eligible. GitHub starts the reusable workflow only when the +pull request changes a configured path and a maintainer adds `docs-preview`. +The caller deliberately listens only for `labeled` events and the shared +workflow independently verifies the label, action, pull request number, and +default target branch. ### `claude-docs-drift.yml` - Dispatch centralized docs drift checks diff --git a/examples/caller-remote-docs-preview.yml b/examples/caller-remote-docs-preview.yml index 3e15cc0..b72f7ec 100644 --- a/examples/caller-remote-docs-preview.yml +++ b/examples/caller-remote-docs-preview.yml @@ -12,6 +12,10 @@ run-name: Preview documentation for PR #${{ github.event.pull_request.number }} on: pull_request_target: types: [labeled] + # Configure this for the directory exported by this repository. Remove the + # paths filter if every pull request should be eligible for a docs preview. + paths: + - "docs/**" permissions: contents: read @@ -19,6 +23,8 @@ permissions: jobs: preview: + # Both conditions must hold: the PR changes the configured paths above and + # a maintainer has explicitly approved this SHA for preview. if: github.event.label.name == 'docs-preview' uses: ClickHouse/integrations-shared-workflows/.github/workflows/remote-docs-preview.yml@main with: From d8677d1d56540955eecb6393a465fc87d7a2a671 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:58:03 +0200 Subject: [PATCH 5/8] Support persistent remote docs preview labels --- .github/workflows/remote-docs-preview.yml | 83 ++++++++++++++++++----- README.md | 26 +++---- examples/caller-remote-docs-preview.yml | 11 +-- 3 files changed, 85 insertions(+), 35 deletions(-) diff --git a/.github/workflows/remote-docs-preview.yml b/.github/workflows/remote-docs-preview.yml index 042768d..20b33c6 100644 --- a/.github/workflows/remote-docs-preview.yml +++ b/.github/workflows/remote-docs-preview.yml @@ -1,10 +1,10 @@ name: Deploy remote documentation preview -# A maintainer approves a source pull request by adding the `docs-preview` -# label. The trusted pull_request_target caller invokes this workflow, which -# resolves the approved PR revision and creates a Vercel deployment of trusted -# Nimbus main. The deployment—not Actions—uses Vercel Connect to obtain a -# short-lived token for the selected remote repository. +# A maintainer authorizes previews for a source pull request by adding the +# `docs-preview` label. The trusted pull_request_target caller invokes this +# workflow for that event and later commits while the label remains. It deploys +# trusted Nimbus main; the deployment—not Actions—uses Vercel Connect to obtain +# a short-lived token for the registered source repository. on: workflow_call: @@ -51,6 +51,9 @@ jobs: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} EVENT_ACTION: ${{ github.event.action }} EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }} + EVENT_HAS_DOCS_PREVIEW_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'docs-preview') }} + EVENT_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} EVENT_LABEL: ${{ github.event.label.name }} DOCS_REPOSITORY: ClickHouse/mintlify-docs-dev EVENT_NAME: ${{ github.event_name }} @@ -62,10 +65,28 @@ jobs: run: | set -euo pipefail - if [[ "$EVENT_NAME" != "pull_request_target" || "$EVENT_ACTION" != "labeled" || "$EVENT_LABEL" != "docs-preview" ]]; then - echo "::error::Remote documentation previews require a pull_request_target labeled event for docs-preview." + if [[ "$EVENT_NAME" != "pull_request_target" ]]; then + echo "::error::Remote documentation previews require a pull_request_target event." exit 1 fi + case "$EVENT_ACTION" in + labeled) + if [[ "$EVENT_LABEL" != "docs-preview" || "$EVENT_HAS_DOCS_PREVIEW_LABEL" != "true" ]]; then + echo "::error::A labeled event must add the docs-preview label." + exit 1 + fi + ;; + synchronize) + if [[ "$EVENT_HAS_DOCS_PREVIEW_LABEL" != "true" ]]; then + echo "::error::A synchronize event requires the docs-preview label." + exit 1 + fi + ;; + *) + echo "::error::Remote documentation previews require a docs-preview labeled or synchronize event." + exit 1 + ;; + esac if [[ "$EVENT_BASE_REF" != "$DEFAULT_BRANCH" ]]; then echo "::error::The pull request must target the repository's default branch." exit 1 @@ -89,25 +110,33 @@ jobs: exit 1 fi - head_sha="$(jq -er .head.sha <<< "$pull_request")" - head_repository="$(jq -er .head.repo.full_name <<< "$pull_request")" + live_head_sha="$(jq -er .head.sha <<< "$pull_request")" + live_head_repository="$(jq -er .head.repo.full_name <<< "$pull_request")" site_sha="$(gh api "repos/$DOCS_REPOSITORY/commits/main" --jq .sha)" - if [[ ! "$head_sha" =~ ^[0-9a-f]{40}$ ]]; then - echo "::error::GitHub returned an invalid pull-request head SHA." + if [[ ! "$EVENT_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::The event payload contains an invalid pull-request head SHA." exit 1 fi - if [[ ! "$head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then - echo "::error::GitHub returned an invalid pull-request head repository." + if [[ ! "$EVENT_HEAD_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo "::error::The event payload contains an invalid pull-request head repository." exit 1 fi if [[ ! "$site_sha" =~ ^[0-9a-f]{40}$ ]]; then echo "::error::GitHub returned an invalid central docs SHA." exit 1 fi + if [[ "$live_head_sha" != "$EVENT_HEAD_SHA" || "$live_head_repository" != "$EVENT_HEAD_REPOSITORY" ]]; then + echo "::error::The pull request head changed after this event; wait for the next event before creating a preview." + exit 1 + fi + if ! jq -e --arg label "docs-preview" 'any(.labels[]; .name == $label)' <<< "$pull_request" >/dev/null; then + echo "::error::The docs-preview label was removed before this preview could start." + exit 1 + fi { - echo "head_sha=$head_sha" - echo "head_repository=$head_repository" + echo "head_sha=$EVENT_HEAD_SHA" + echo "head_repository=$EVENT_HEAD_REPOSITORY" echo "site_sha=$site_sha" } >> "$GITHUB_OUTPUT" @@ -122,6 +151,7 @@ jobs: DOCS_REMOTE_REF: ${{ steps.request.outputs.head_sha }} DOCS_REMOTE_REPOSITORY: ${{ github.repository }} DOCS_REMOTE_SOURCE_REPOSITORY: ${{ steps.request.outputs.head_repository }} + DOCS_REMOTE_PULL_REQUEST_REF: refs/pull/${{ inputs.pull_request_number }}/head PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} SITE_SHA: ${{ steps.request.outputs.site_sha }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} @@ -132,6 +162,9 @@ jobs: deployment_id="" deployment_finished=false + # Leave enough of the 60-minute job budget to cancel a timed-out + # deployment and report a useful error rather than being hard-killed. + poll_deadline=$((SECONDS + 3000)) cancel_vercel_deployment() { local deployment_id_to_cancel="$1" @@ -237,7 +270,10 @@ jobs: finalize_deployment() { local exit_code=$? - trap - EXIT INT TERM + trap - EXIT + # GitHub sends TERM shortly after INT. Keep the cleanup request + # alive long enough to cancel the Vercel deployment. + trap '' INT TERM if [[ "$deployment_finished" != true && -n "$deployment_id" ]]; then if ! cancel_vercel_deployment "$deployment_id" "source-preview"; then echo "::warning::Unable to cancel the interrupted source-preview deployment." @@ -274,6 +310,7 @@ jobs: --arg remote_name "$DOCS_REMOTE_NAME" \ --arg remote_repository "$DOCS_REMOTE_REPOSITORY" \ --arg source_repository "$DOCS_REMOTE_SOURCE_REPOSITORY" \ + --arg pull_request_ref "$DOCS_REMOTE_PULL_REQUEST_REF" \ --arg site_sha "$SITE_SHA" \ '{ name: $project_name, @@ -294,6 +331,7 @@ jobs: DOCS_REMOTE_NAME: $remote_name, DOCS_REMOTE_REPOSITORY: $remote_repository, DOCS_REMOTE_SOURCE_REPOSITORY: $source_repository, + DOCS_REMOTE_PULL_REQUEST_REF: $pull_request_ref, DOCS_REMOTE_REF: $approved_sha }}, meta: { @@ -325,7 +363,7 @@ jobs: echo "preview_url=$preview_url" >> "$GITHUB_OUTPUT" echo "Vercel deployment: $preview_url" - for _ in {1..360}; do + while (( SECONDS < poll_deadline )); do if ! deployment_response="$( curl --silent --show-error --fail-with-body \ --header "Authorization: Bearer $VERCEL_TOKEN" \ @@ -346,7 +384,16 @@ jobs: exit 1 ;; QUEUED|INITIALIZING|BUILDING) - sleep 10 + remaining_seconds=$((poll_deadline - SECONDS)) + if (( remaining_seconds <= 0 )); then + break + fi + sleep_seconds=10 + if (( remaining_seconds < sleep_seconds )); then + sleep_seconds=$remaining_seconds + fi + sleep "$sleep_seconds" & + wait "$!" ;; *) echo "Unexpected Vercel deployment state: $ready_state" >&2 diff --git a/README.md b/README.md index 369ca90..044d7bf 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,21 @@ so the logic lives in one place and per-repo specifics are passed as inputs. ### `remote-docs-preview.yml` - Request a remote documentation preview Starts a scoped documentation preview for a pull request in a repository -registered by `ClickHouse/mintlify-docs-dev/remotes.json`. A maintainer invokes -the caller by adding the `docs-preview` label to a pull request targeting the -remote repository's default branch. The shared workflow validates the trusted -label event, pins the pull request's exact head SHA, and directly +registered by `ClickHouse/mintlify-docs-dev/remotes.json`. A maintainer enables +previews by adding the `docs-preview` label to a pull request targeting the +remote repository's default branch. The label remains effective until removed, +so later commits refresh the preview. Each run validates its trusted event and +pins that event's exact head SHA, then directly creates a Vercel deployment of the current trusted Nimbus `main` revision in the `connect-preview` Custom Environment. The workflow never checks out or executes pull-request content and never passes a GitHub credential to Vercel. Inside the deployment, Vercel Connect exchanges the deployment's OIDC identity for a short-lived `contents:read` token scoped to -the selected repository. Nimbus fetches the approved revision before it removes -the token and begins processing Markdown or MDX. The shared workflow waits for -the deployment and comments the resulting URL on the source pull request. +the registered source repository. Nimbus fetches its `refs/pull//head` +ref, verifies that it resolves to the event SHA, then removes the token before +it begins processing Markdown or MDX. The shared workflow waits for the +deployment and comments the resulting URL on the source pull request. | Input | Required | Purpose | |---|---|---| @@ -39,11 +41,11 @@ See [`examples/caller-remote-docs-preview.yml`](examples/caller-remote-docs-prev for a copy-paste `pull_request_target` caller. Set `remote_name` to the source's registered name and configure the caller's native `paths` filter for the files that should be eligible, such as `docs/**`. Remove `paths` when every pull -request should be eligible. GitHub starts the reusable workflow only when the -pull request changes a configured path and a maintainer adds `docs-preview`. -The caller deliberately listens only for `labeled` events and the shared -workflow independently verifies the label, action, pull request number, and -default target branch. +request should be eligible. GitHub starts the reusable workflow when the pull +request changes a configured path, a maintainer adds `docs-preview`, or the +labelled pull request receives a new commit. The caller listens only for +`labeled` and `synchronize` events; the shared workflow independently verifies +the label, action, pull request number, event SHA, and default target branch. ### `claude-docs-drift.yml` - Dispatch centralized docs drift checks diff --git a/examples/caller-remote-docs-preview.yml b/examples/caller-remote-docs-preview.yml index b72f7ec..2132363 100644 --- a/examples/caller-remote-docs-preview.yml +++ b/examples/caller-remote-docs-preview.yml @@ -1,17 +1,18 @@ # Install as .github/workflows/docs-preview.yml in a repository registered as a # remote documentation source in ClickHouse/mintlify-docs-dev/remotes.json. # -# A maintainer approves a pull request by adding the `docs-preview` label. This +# A maintainer enables previews for a pull request by adding the `docs-preview` +# label. While the label remains, each new commit refreshes the preview. This # uses pull_request_target so organization secrets are available for fork PRs, # but neither this caller nor the shared workflow checks out or executes PR -# code. Vercel Connect fetches the approved remote revision inside the build. +# code. Vercel Connect fetches the base repository's PR ref inside the build. name: Preview documentation run-name: Preview documentation for PR #${{ github.event.pull_request.number }} on: pull_request_target: - types: [labeled] + types: [labeled, synchronize] # Configure this for the directory exported by this repository. Remove the # paths filter if every pull request should be eligible for a docs preview. paths: @@ -24,8 +25,8 @@ permissions: jobs: preview: # Both conditions must hold: the PR changes the configured paths above and - # a maintainer has explicitly approved this SHA for preview. - if: github.event.label.name == 'docs-preview' + # a maintainer has enabled previews by applying docs-preview. + if: contains(github.event.pull_request.labels.*.name, 'docs-preview') uses: ClickHouse/integrations-shared-workflows/.github/workflows/remote-docs-preview.yml@main with: # This must match the source's name in the central remotes.json registry. From 809bd8c0fb5b13e317a5e48a82df13cd6bb131b6 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:02:19 +0200 Subject: [PATCH 6/8] Harden remote preview deployment handling --- .github/workflows/remote-docs-preview.yml | 38 +++++++++++++++-------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/.github/workflows/remote-docs-preview.yml b/.github/workflows/remote-docs-preview.yml index 20b33c6..ef2e40f 100644 --- a/.github/workflows/remote-docs-preview.yml +++ b/.github/workflows/remote-docs-preview.yml @@ -162,9 +162,10 @@ jobs: deployment_id="" deployment_finished=false - # Leave enough of the 60-minute job budget to cancel a timed-out - # deployment and report a useful error rather than being hard-killed. - poll_deadline=$((SECONDS + 3000)) + # Vercel caps this environment's builds at 40 minutes. Stop polling + # at that limit while preserving job time for cancellation and a + # useful failure report. + poll_deadline=$((SECONDS + 2400)) cancel_vercel_deployment() { local deployment_id_to_cancel="$1" @@ -211,6 +212,7 @@ jobs: local list_response local next local superseded_id + local deployment_ids_json local -a request_args local -a deployment_ids @@ -240,19 +242,29 @@ jobs: return 1 fi - mapfile -t deployment_ids < <( - jq -r \ + if ! deployment_ids_json="$( + jq -ce \ --arg pull_request "$PULL_REQUEST_NUMBER" \ --arg source_repository "$DOCS_REMOTE_REPOSITORY" \ - '.deployments[] + '[.deployments[] | select((.meta.buildScope? // "") == "source-preview") | select((.meta.sourceRepository? // "") == $source_repository) | select((.meta.pullRequest? // "") == $pull_request) - | (.uid // .id)' \ + | (.uid // .id)]' \ <<< "$list_response" - ) + )"; then + echo "Vercel returned an invalid deployment list." >&2 + return 1 + fi + + deployment_ids=() + if [[ "$deployment_ids_json" != "[]" ]]; then + mapfile -t deployment_ids < <(jq -r '.[]' <<< "$deployment_ids_json") + fi for superseded_id in "${deployment_ids[@]}"; do - cancel_vercel_deployment "$superseded_id" "superseded source-preview" || return 1 + if ! cancel_vercel_deployment "$superseded_id" "superseded source-preview"; then + echo "::warning::Unable to cancel superseded source-preview deployment $superseded_id; continuing with the new preview." + fi done next="$(jq -er '.pagination.next // ""' <<< "$list_response")" @@ -360,6 +372,10 @@ jobs: deployment_id="$(jq -er .id <<< "$deployment_response")" deployment_url="$(jq -er .url <<< "$deployment_response")" preview_url="https://${deployment_url#https://}" + if [[ ! "$preview_url" =~ ^https://[^[:space:]]+$ ]]; then + echo "Vercel returned an invalid preview URL." >&2 + exit 1 + fi echo "preview_url=$preview_url" >> "$GITHUB_OUTPUT" echo "Vercel deployment: $preview_url" @@ -406,10 +422,6 @@ jobs: echo "Timed out waiting for Vercel deployment $deployment_id." >&2 exit 1 fi - if [[ ! "$preview_url" =~ ^https://[^[:space:]]+$ ]]; then - echo "Vercel returned an invalid preview URL." >&2 - exit 1 - fi deployment_finished=true - name: Add the preview link to the pull request From 74e2a5b8e15017221da66d5973feeb8b8a86a4cb Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:06:19 +0200 Subject: [PATCH 7/8] Clean up remote docs preview configuration --- .github/workflows/remote-docs-preview.yml | 42 +++++++++++++---------- .github/zizmor.yml | 6 +--- examples/caller-remote-docs-preview.yml | 9 ++--- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/.github/workflows/remote-docs-preview.yml b/.github/workflows/remote-docs-preview.yml index ef2e40f..935ff2d 100644 --- a/.github/workflows/remote-docs-preview.yml +++ b/.github/workflows/remote-docs-preview.yml @@ -1,10 +1,7 @@ name: Deploy remote documentation preview -# A maintainer authorizes previews for a source pull request by adding the -# `docs-preview` label. The trusted pull_request_target caller invokes this -# workflow for that event and later commits while the label remains. It deploys -# trusted Nimbus main; the deployment—not Actions—uses Vercel Connect to obtain -# a short-lived token for the registered source repository. +# The authorization and Vercel Connect security model are documented in the +# remote-docs-preview section of README.md. on: workflow_call: @@ -36,8 +33,10 @@ jobs: name: Build docs preview on Vercel runs-on: ubuntu-latest timeout-minutes: 60 + env: + DOCS_REPOSITORY: ClickHouse/mintlify-docs-dev + DOCS_REF: main permissions: - contents: read pull-requests: write concurrency: group: remote-docs-preview-${{ github.repository }}-${{ inputs.pull_request_number }} @@ -55,7 +54,6 @@ jobs: EVENT_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} EVENT_LABEL: ${{ github.event.label.name }} - DOCS_REPOSITORY: ClickHouse/mintlify-docs-dev EVENT_NAME: ${{ github.event_name }} EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_TOKEN: ${{ github.token }} @@ -112,7 +110,7 @@ jobs: live_head_sha="$(jq -er .head.sha <<< "$pull_request")" live_head_repository="$(jq -er .head.repo.full_name <<< "$pull_request")" - site_sha="$(gh api "repos/$DOCS_REPOSITORY/commits/main" --jq .sha)" + site_sha="$(gh api "repos/$DOCS_REPOSITORY/commits/$DOCS_REF" --jq .sha)" if [[ ! "$EVENT_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then echo "::error::The event payload contains an invalid pull-request head SHA." exit 1 @@ -140,10 +138,7 @@ jobs: echo "site_sha=$site_sha" } >> "$GITHUB_OUTPUT" - # The payload contains no GitHub credential. Vercel fetches the trusted - # Nimbus revision through the project's Git connection. During that - # build, @vercel/connect exchanges VERCEL_OIDC_TOKEN for a short-lived, - # contents:read token scoped to the selected source repository. + # The deployment security model is documented in README.md. - name: Trigger and wait for the Vercel Connect preview id: deploy env: @@ -312,6 +307,12 @@ jobs: exit 1 fi project_name="$(jq -er .name <<< "$project_response")" + docs_owner="${DOCS_REPOSITORY%%/*}" + docs_repo="${DOCS_REPOSITORY##*/}" + if [[ ! "$DOCS_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo "Invalid central docs repository: $DOCS_REPOSITORY" >&2 + exit 1 + fi payload="$( jq --null-input \ @@ -324,15 +325,18 @@ jobs: --arg source_repository "$DOCS_REMOTE_SOURCE_REPOSITORY" \ --arg pull_request_ref "$DOCS_REMOTE_PULL_REQUEST_REF" \ --arg site_sha "$SITE_SHA" \ + --arg docs_owner "$docs_owner" \ + --arg docs_repo "$docs_repo" \ + --arg docs_ref "$DOCS_REF" \ '{ name: $project_name, project: $project_id, customEnvironmentSlugOrId: "connect-preview", gitSource: { type: "github", - org: "ClickHouse", - repo: "mintlify-docs-dev", - ref: "main", + org: $docs_owner, + repo: $docs_repo, + ref: $docs_ref, sha: $site_sha }, build: {env: { @@ -430,15 +434,17 @@ jobs: GH_REPO: ${{ github.repository }} PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }} PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} + REMOTE_NAME: ${{ inputs.remote_name }} + REMOTE_SHA: ${{ steps.request.outputs.head_sha }} run: | set -euo pipefail - marker="" + marker="" docs_url="${PREVIEW_URL%/}/docs" - body="$(printf '%s\n\nDocumentation preview: %s' "$marker" "$docs_url")" + body="$(printf '%s\n\nDocumentation preview for %s: %s' "$marker" "$REMOTE_SHA" "$docs_url")" comment_id="$( gh api --paginate "repos/$GH_REPO/issues/$PULL_REQUEST_NUMBER/comments" \ - --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"$marker\")) | .id" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | startswith(\"$marker\")) | .id" \ | tail -n 1 )" if [[ -n "$comment_id" ]]; then diff --git a/.github/zizmor.yml b/.github/zizmor.yml index f26bf35..f5ee20a 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -14,11 +14,7 @@ rules: dangerous-triggers: ignore: - # This caller intentionally uses pull_request_target so a maintainer can - # authorize a fork preview by adding `docs-preview`. It accepts only the - # labeled event, checks the exact label again in the reusable workflow, - # and never checks out or executes pull-request content. Vercel Connect - # fetches the approved SHA and credentials are removed before MDX runs. + # See the remote-docs-preview security model in README.md. - caller-remote-docs-preview.yml unpinned-uses: ignore: diff --git a/examples/caller-remote-docs-preview.yml b/examples/caller-remote-docs-preview.yml index 2132363..e6d9750 100644 --- a/examples/caller-remote-docs-preview.yml +++ b/examples/caller-remote-docs-preview.yml @@ -2,10 +2,8 @@ # remote documentation source in ClickHouse/mintlify-docs-dev/remotes.json. # # A maintainer enables previews for a pull request by adding the `docs-preview` -# label. While the label remains, each new commit refreshes the preview. This -# uses pull_request_target so organization secrets are available for fork PRs, -# but neither this caller nor the shared workflow checks out or executes PR -# code. Vercel Connect fetches the base repository's PR ref inside the build. +# label. While the label remains, each new commit refreshes the preview. See +# README.md for the pull_request_target and Vercel Connect security model. name: Preview documentation run-name: Preview documentation for PR #${{ github.event.pull_request.number }} @@ -19,7 +17,6 @@ on: - "docs/**" permissions: - contents: read pull-requests: write jobs: @@ -30,7 +27,7 @@ jobs: uses: ClickHouse/integrations-shared-workflows/.github/workflows/remote-docs-preview.yml@main with: # This must match the source's name in the central remotes.json registry. - remote_name: clickhouse-private + remote_name: example-docs pull_request_number: ${{ github.event.pull_request.number }} secrets: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} From 679beea1ccef6bf5ba82fd516831a3ba7567ad9e Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:44:01 +0200 Subject: [PATCH 8/8] Harden remote preview lifecycle --- .github/workflows/remote-docs-preview.yml | 186 +++++++++++++--------- README.md | 10 +- examples/caller-remote-docs-preview.yml | 6 +- 3 files changed, 117 insertions(+), 85 deletions(-) diff --git a/.github/workflows/remote-docs-preview.yml b/.github/workflows/remote-docs-preview.yml index 935ff2d..f34f3b6 100644 --- a/.github/workflows/remote-docs-preview.yml +++ b/.github/workflows/remote-docs-preview.yml @@ -39,7 +39,7 @@ jobs: permissions: pull-requests: write concurrency: - group: remote-docs-preview-${{ github.repository }}-${{ inputs.pull_request_number }} + group: remote-docs-preview-${{ github.repository }}-${{ inputs.remote_name }}-${{ inputs.pull_request_number }} cancel-in-progress: true outputs: preview_url: ${{ steps.deploy.outputs.preview_url }} @@ -80,8 +80,14 @@ jobs: exit 1 fi ;; + reopened) + if [[ "$EVENT_HAS_DOCS_PREVIEW_LABEL" != "true" ]]; then + echo "::error::A reopened event requires the docs-preview label." + exit 1 + fi + ;; *) - echo "::error::Remote documentation previews require a docs-preview labeled or synchronize event." + echo "::error::Remote documentation previews require a docs-preview labeled, synchronize, or reopened event." exit 1 ;; esac @@ -157,10 +163,15 @@ jobs: deployment_id="" deployment_finished=false - # Vercel caps this environment's builds at 40 minutes. Stop polling - # at that limit while preserving job time for cancellation and a - # useful failure report. - poll_deadline=$((SECONDS + 2400)) + # Reserve five minutes of this 60-minute job for cancellation and a + # useful failure report. The 40-minute Vercel build limit starts when + # Vercel reports BUILDING, rather than charging queue time against it. + job_deadline=$((SECONDS + 3300)) + build_deadline=0 + + curl_request() { + curl --connect-timeout 10 --max-time 30 "$@" + } cancel_vercel_deployment() { local deployment_id_to_cancel="$1" @@ -171,7 +182,7 @@ jobs: local ready_state if cancel_response="$( - curl --silent --show-error --fail-with-body \ + curl_request --silent --show-error --fail-with-body \ --request PATCH \ --header "Authorization: Bearer $VERCEL_TOKEN" \ "https://api.vercel.com/v12/deployments/$deployment_id_to_cancel/cancel?teamId=$VERCEL_ORG_ID" @@ -183,7 +194,7 @@ jobs: fi if deployment_response="$( - curl --silent --show-error --fail-with-body \ + curl_request --silent --show-error --fail-with-body \ --header "Authorization: Bearer $VERCEL_TOKEN" \ "https://api.vercel.com/v13/deployments/$deployment_id_to_cancel?teamId=$VERCEL_ORG_ID" )"; then @@ -202,7 +213,6 @@ jobs: } cancel_superseded_deployments() { - local ready_state local until="" local list_response local next @@ -211,67 +221,66 @@ jobs: local -a request_args local -a deployment_ids - for ready_state in QUEUED INITIALIZING BUILDING; do - until="" - while true; do - request_args=( - --silent - --show-error - --fail-with-body - --get - --header "Authorization: Bearer $VERCEL_TOKEN" - --data-urlencode "projectId=$VERCEL_PROJECT_ID" - --data-urlencode "state=$ready_state" - --data-urlencode "limit=100" - --data-urlencode "teamId=$VERCEL_ORG_ID" - ) - if [[ -n "$until" ]]; then - request_args+=(--data-urlencode "until=$until") - fi + while true; do + request_args=( + --silent + --show-error + --fail-with-body + --get + --header "Authorization: Bearer $VERCEL_TOKEN" + --data-urlencode "projectId=$VERCEL_PROJECT_ID" + --data-urlencode "state=QUEUED,INITIALIZING,BUILDING" + --data-urlencode "limit=100" + --data-urlencode "teamId=$VERCEL_ORG_ID" + ) + if [[ -n "$until" ]]; then + request_args+=(--data-urlencode "until=$until") + fi - if ! list_response="$( - curl "${request_args[@]}" "https://api.vercel.com/v7/deployments" - )"; then - printf '%s\n' "$list_response" >&2 - echo "Unable to list source-preview Vercel deployments." >&2 - return 1 - fi + if ! list_response="$( + curl_request "${request_args[@]}" "https://api.vercel.com/v7/deployments" + )"; then + printf '%s\n' "$list_response" >&2 + echo "Unable to list source-preview Vercel deployments." >&2 + return 1 + fi - if ! deployment_ids_json="$( - jq -ce \ - --arg pull_request "$PULL_REQUEST_NUMBER" \ - --arg source_repository "$DOCS_REMOTE_REPOSITORY" \ - '[.deployments[] - | select((.meta.buildScope? // "") == "source-preview") - | select((.meta.sourceRepository? // "") == $source_repository) - | select((.meta.pullRequest? // "") == $pull_request) - | (.uid // .id)]' \ - <<< "$list_response" - )"; then - echo "Vercel returned an invalid deployment list." >&2 - return 1 - fi + if ! deployment_ids_json="$( + jq -ce \ + --arg pull_request "$PULL_REQUEST_NUMBER" \ + --arg remote_name "$DOCS_REMOTE_NAME" \ + --arg source_repository "$DOCS_REMOTE_REPOSITORY" \ + '[.deployments[] + | select((.meta.buildScope? // "") == "source-preview") + | select((.meta.sourceRepository? // "") == $source_repository) + | select((.meta.remoteName? // "") == $remote_name) + | select((.meta.pullRequest? // "") == $pull_request) + | (.uid // .id)]' \ + <<< "$list_response" + )"; then + echo "Vercel returned an invalid deployment list." >&2 + return 1 + fi - deployment_ids=() - if [[ "$deployment_ids_json" != "[]" ]]; then - mapfile -t deployment_ids < <(jq -r '.[]' <<< "$deployment_ids_json") - fi - for superseded_id in "${deployment_ids[@]}"; do - if ! cancel_vercel_deployment "$superseded_id" "superseded source-preview"; then - echo "::warning::Unable to cancel superseded source-preview deployment $superseded_id; continuing with the new preview." - fi - done - - next="$(jq -er '.pagination.next // ""' <<< "$list_response")" - if [[ -z "$next" ]]; then - break - fi - if [[ "$next" == "$until" ]]; then - echo "Vercel returned a repeated pagination cursor while listing source-preview deployments." >&2 - return 1 + deployment_ids=() + if [[ "$deployment_ids_json" != "[]" ]]; then + mapfile -t deployment_ids < <(jq -r '.[]' <<< "$deployment_ids_json") + fi + for superseded_id in "${deployment_ids[@]}"; do + if ! cancel_vercel_deployment "$superseded_id" "superseded source-preview"; then + echo "::warning::Unable to cancel superseded source-preview deployment $superseded_id; continuing with the new preview." fi - until="$next" done + + next="$(jq -er '.pagination.next // ""' <<< "$list_response")" + if [[ -z "$next" ]]; then + break + fi + if [[ "$next" == "$until" ]]; then + echo "Vercel returned a repeated pagination cursor while listing source-preview deployments." >&2 + return 1 + fi + until="$next" done } @@ -292,14 +301,16 @@ jobs: exit "$exit_code" } - cancel_superseded_deployments + if ! cancel_superseded_deployments; then + echo "::warning::Unable to cancel superseded source-preview deployments; continuing with the new preview." + fi trap finalize_deployment EXIT trap 'exit 130' INT trap 'exit 143' TERM if ! project_response="$( - curl --silent --show-error --fail-with-body \ + curl_request --silent --show-error --fail-with-body \ --header "Authorization: Bearer $VERCEL_TOKEN" \ "https://api.vercel.com/v9/projects/$VERCEL_PROJECT_ID?teamId=$VERCEL_ORG_ID" )"; then @@ -354,6 +365,7 @@ jobs: buildScope: "source-preview", sourceRepository: $remote_repository, sourceHeadRepository: $source_repository, + remoteName: $remote_name, pullRequest: $pull_request, approvedSha: $approved_sha, siteSha: $site_sha @@ -362,7 +374,7 @@ jobs: )" if ! deployment_response="$( - curl --silent --show-error --fail-with-body \ + curl_request --silent --show-error --fail-with-body \ --request POST \ --header "Authorization: Bearer $VERCEL_TOKEN" \ --header "Content-Type: application/json" \ @@ -380,12 +392,12 @@ jobs: echo "Vercel returned an invalid preview URL." >&2 exit 1 fi - echo "preview_url=$preview_url" >> "$GITHUB_OUTPUT" echo "Vercel deployment: $preview_url" - while (( SECONDS < poll_deadline )); do + ready_state="" + while (( SECONDS < job_deadline && (build_deadline == 0 || SECONDS < build_deadline) )); do if ! deployment_response="$( - curl --silent --show-error --fail-with-body \ + curl_request --silent --show-error --fail-with-body \ --header "Authorization: Bearer $VERCEL_TOKEN" \ "https://api.vercel.com/v13/deployments/$deployment_id?teamId=$VERCEL_ORG_ID" )"; then @@ -399,12 +411,32 @@ jobs: break ;; ERROR|CANCELED|DELETED|BLOCKED) + deployment_finished=true jq -r '(.errorCode // "deployment_failed") + ": " + (.errorMessage // "Vercel deployment did not complete")' \ <<< "$deployment_response" >&2 exit 1 ;; + QUEUED|INITIALIZING) + wake_deadline=$job_deadline + ;; + BUILDING) + if (( build_deadline == 0 )); then + build_deadline=$((SECONDS + 2400)) + fi + wake_deadline=$build_deadline + if (( job_deadline < wake_deadline )); then + wake_deadline=$job_deadline + fi + ;; + *) + echo "Unexpected Vercel deployment state: $ready_state" >&2 + exit 1 + ;; + esac + + case "$ready_state" in QUEUED|INITIALIZING|BUILDING) - remaining_seconds=$((poll_deadline - SECONDS)) + remaining_seconds=$((wake_deadline - SECONDS)) if (( remaining_seconds <= 0 )); then break fi @@ -415,10 +447,6 @@ jobs: sleep "$sleep_seconds" & wait "$!" ;; - *) - echo "Unexpected Vercel deployment state: $ready_state" >&2 - exit 1 - ;; esac done @@ -427,6 +455,7 @@ jobs: exit 1 fi deployment_finished=true + echo "preview_url=$preview_url" >> "$GITHUB_OUTPUT" - name: Add the preview link to the pull request env: @@ -439,12 +468,13 @@ jobs: run: | set -euo pipefail - marker="" + marker="" docs_url="${PREVIEW_URL%/}/docs" body="$(printf '%s\n\nDocumentation preview for %s: %s' "$marker" "$REMOTE_SHA" "$docs_url")" comment_id="$( gh api --paginate "repos/$GH_REPO/issues/$PULL_REQUEST_NUMBER/comments" \ - --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | startswith(\"$marker\")) | .id" \ + | jq -r --arg marker "$marker" \ + '.[] | select(.user.login == "github-actions[bot]") | select(.body | startswith($marker)) | .id' \ | tail -n 1 )" if [[ -n "$comment_id" ]]; then diff --git a/README.md b/README.md index 044d7bf..41ffc3d 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,11 @@ See [`examples/caller-remote-docs-preview.yml`](examples/caller-remote-docs-prev for a copy-paste `pull_request_target` caller. Set `remote_name` to the source's registered name and configure the caller's native `paths` filter for the files that should be eligible, such as `docs/**`. Remove `paths` when every pull -request should be eligible. GitHub starts the reusable workflow when the pull -request changes a configured path, a maintainer adds `docs-preview`, or the -labelled pull request receives a new commit. The caller listens only for -`labeled` and `synchronize` events; the shared workflow independently verifies -the label, action, pull request number, event SHA, and default target branch. +request should be eligible. The configured `paths` filter applies to every +eligible event. The caller listens for `labeled`, `synchronize`, and `reopened`; +the job runs only when the PR matches that filter and either adds `docs-preview` +or already has it. The shared workflow independently verifies the label, action, +pull request number, event SHA, and default target branch. ### `claude-docs-drift.yml` - Dispatch centralized docs drift checks diff --git a/examples/caller-remote-docs-preview.yml b/examples/caller-remote-docs-preview.yml index e6d9750..ae0b7ec 100644 --- a/examples/caller-remote-docs-preview.yml +++ b/examples/caller-remote-docs-preview.yml @@ -10,7 +10,7 @@ run-name: Preview documentation for PR #${{ github.event.pull_request.number }} on: pull_request_target: - types: [labeled, synchronize] + types: [labeled, synchronize, reopened] # Configure this for the directory exported by this repository. Remove the # paths filter if every pull request should be eligible for a docs preview. paths: @@ -23,7 +23,9 @@ jobs: preview: # Both conditions must hold: the PR changes the configured paths above and # a maintainer has enabled previews by applying docs-preview. - if: contains(github.event.pull_request.labels.*.name, 'docs-preview') + if: >- + contains(github.event.pull_request.labels.*.name, 'docs-preview') && + (github.event.action != 'labeled' || github.event.label.name == 'docs-preview') uses: ClickHouse/integrations-shared-workflows/.github/workflows/remote-docs-preview.yml@main with: # This must match the source's name in the central remotes.json registry.