diff --git a/.github/workflows/remote-docs-preview.yml b/.github/workflows/remote-docs-preview.yml index 260261022..d3c863a40 100644 --- a/.github/workflows/remote-docs-preview.yml +++ b/.github/workflows/remote-docs-preview.yml @@ -17,6 +17,11 @@ on: description: Open pull request to preview required: true type: string + approved_head_sha: + description: Exact pull request head SHA approved by the dispatching workflow + required: false + default: "" + type: string workflow_call: inputs: remote_name: @@ -31,6 +36,11 @@ on: description: Repository containing the pull request required: true type: string + approved_head_sha: + description: Exact pull request head SHA approved by the dispatching workflow + required: false + default: "" + type: string secrets: WORKFLOW_AUTH_PUBLIC_APP_ID: required: true @@ -79,7 +89,7 @@ jobs: fi - name: Check out the trusted documentation site - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: repository: ClickHouse/mintlify-docs-dev ref: main @@ -87,7 +97,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 @@ -112,21 +122,34 @@ jobs: fi echo "sha=$site_sha" >> "$GITHUB_OUTPUT" + - name: Resolve the source repository + id: source + env: + SOURCE_REPOSITORY: ${{ inputs.source_repository }} + run: | + if [[ ! "$SOURCE_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo "Source repository must use the owner/name form." >&2 + exit 1 + fi + echo "owner=${SOURCE_REPOSITORY%%/*}" >> "$GITHUB_OUTPUT" + echo "name=${SOURCE_REPOSITORY#*/}" >> "$GITHUB_OUTPUT" + - name: Mint a source-repository token id: source-token - uses: actions/create-github-app-token@v3 + 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: ${{ inputs.source_repository }} + owner: ${{ steps.source.outputs.owner }} + repositories: ${{ steps.source.outputs.name }} permission-contents: read - permission-pull-requests: read + permission-pull-requests: write - name: Resolve the pull request head id: pull-request env: GH_TOKEN: ${{ steps.source-token.outputs.token }} + APPROVED_HEAD_SHA: ${{ inputs.approved_head_sha }} PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} SOURCE_REPOSITORY: ${{ inputs.source_repository }} run: | @@ -142,6 +165,16 @@ jobs: echo "GitHub returned an invalid pull request head SHA." >&2 exit 1 fi + if [[ -n "$APPROVED_HEAD_SHA" ]]; then + if [[ ! "$APPROVED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "The dispatching workflow supplied an invalid approved head SHA." >&2 + exit 1 + fi + if [[ "$head_sha" != "$APPROVED_HEAD_SHA" ]]; then + echo "Pull request head changed after preview approval: expected $APPROVED_HEAD_SHA, found $head_sha." >&2 + exit 1 + fi + fi if [[ ! "$head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then echo "GitHub returned an invalid pull request head repository." >&2 exit 1 @@ -402,14 +435,25 @@ jobs: deployment_finished=true - name: Add the preview link to the pull request - if: github.repository == inputs.source_repository env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ steps.source-token.outputs.token }} + GH_REPO: ${{ inputs.source_repository }} PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }} PULL_REQUEST_NUMBER: ${{ inputs.pull_request_number }} run: | - gh pr comment "$PULL_REQUEST_NUMBER" --body "Documentation preview: $PREVIEW_URL" + 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(.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 - name: Show the directly requested preview if: github.repository != inputs.source_repository diff --git a/reports/templates/airgap-docs-preview.yml b/reports/templates/airgap-docs-preview.yml index b0d2582cb..c8acdf857 100644 --- a/reports/templates/airgap-docs-preview.yml +++ b/reports/templates/airgap-docs-preview.yml @@ -21,14 +21,10 @@ permissions: jobs: preview: - uses: ClickHouse/mintlify-docs-dev/.github/workflows/remote-docs-preview.yml@main + uses: ClickHouse/integrations-shared-workflows/.github/workflows/remote-docs-preview.yml@main with: remote_name: clickhouse-private pull_request_number: ${{ fromJSON(inputs.pull_request_number) }} - source_repository: ${{ github.repository }} 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 }} diff --git a/src/README.md b/src/README.md index a99fb70fe..71df5196c 100644 --- a/src/README.md +++ b/src/README.md @@ -52,12 +52,15 @@ Production source topology lives only in `remotes.json`; production fetches each registered repository from `main`. Remote CI supplies the registered name, repository, and exceptional immutable SHA only when requesting a preview. -Remote repositories create previews through -`.github/workflows/remote-docs-preview.yml`. The caller invokes the reusable -workflow manually with a pull request number. It uses a repository-scoped -GitHub App token only to resolve the immutable head SHA and the branch or fork -repository that owns it, then asks Vercel to build trusted Nimbus `main` in the -`connect-preview` environment. The Vercel build uses +Remote repositories create previews through the reusable +`ClickHouse/integrations-shared-workflows/.github/workflows/remote-docs-preview.yml` +dispatcher. The caller invokes it manually with a pull request number. The +shared workflow resolves the immutable head SHA, then uses a repository-scoped +GitHub App token to dispatch `.github/workflows/remote-docs-preview.yml` in this +repository. Vercel credentials remain available only to this central workflow, +which verifies that the approved SHA is still the pull request head before it +asks Vercel to build trusted Nimbus `main` in the `connect-preview` environment. +The Vercel build uses its OIDC identity to request a short-lived, `contents:read` token from Vercel Connect for the branch or fork repository. Public repositories are fetched anonymously. `bin/fetch-remotes.ts` exits before @@ -78,13 +81,14 @@ project's Git connection and updates one preview comment on the pull request. Both primary-repository branches and forks use standard Preview and omit every registered remote source. -Source-repository pull requests use -`.github/workflows/remote-docs-preview.yml`. Vercel builds trusted Nimbus -`main`, selects exactly one registered source with `DOCS_REMOTE_*`, and fetches -the pull request's immutable head revision. Trusted branches and forks have the -same source-only build scope. They use `connect-preview` so private registered -sources can obtain a short-lived token; public sources remain anonymously -fetchable. +Source-repository pull requests use the shared dispatcher described above. The +central `.github/workflows/remote-docs-preview.yml` workflow builds trusted +Nimbus `main`, selects exactly one registered source with `DOCS_REMOTE_*`, and +fetches the pull request's immutable head revision. Trusted branches and forks +have the same source-only build scope. They use `connect-preview` so private +registered sources can obtain a short-lived token; public sources remain +anonymously fetchable. The central workflow posts the completed preview URL +back to the source pull request with a short-lived GitHub App token. After a source-repository change reaches its trusted production branch, that repository calls `.github/workflows/site-production.yml`. The reusable workflow