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/4] 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/4] 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/4] 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/4] 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: