mirror of
https://github.com/coder/coder.git
synced 2026-09-22 21:22:17 +08:00
Closes [DOCS-327](https://linear.app/codercom/issue/DOCS-327/trigger-docs-reindex-on-codercoder-releasepublished). ## What Add `release: { types: [published] }` to `.github/workflows/deploy-docs.yaml` so that publishing a stable `vX.Y.Z` GitHub Release on this repo auto-dispatches the docs-sync handler against the corresponding `release/X.Y` branch. The existing `push` and `workflow_dispatch` triggers are unchanged. The `Compute action and ref` step gains a release-event branch that: - Skips prereleases (`github.event.release.prerelease == true`) with a workflow notice. - Matches the tag against `^v([0-9]+)\.([0-9]+)\.[0-9]+$` and translates `v2.35.0` to `release/2.35`. - Falls through with a notice and `exit 0` for any tag that doesn't match the plain semver shape (`v2.35`, `v2.35.0-rc.1`, etc.). Downstream validation, HMAC body construction, and the POST step are unchanged. The POST step gains an `if: steps.input.outputs.action != ''` guard so the two `exit 0` paths skip the POST instead of sending empty `action`/`ref` to the production handler. A new `.github/workflows/test-deploy-docs-release.sh` exercises the release-event bash against the 11 event scenarios in the table below plus 3 regex boundary cases, mirroring the existing `test-deploy-docs-diff.sh` pattern. ## Why Today, every mainline rollover requires a human to dispatch this workflow manually with `action=index, ref=release/X.Y`. We just hit this rotation friction on [DOCS-324](https://linear.app/codercom/issue/DOCS-324/rotate-algolia-indexer-allowlist-for-v234-launch-add-release234-drop) (v2.34 launch) and the resulting empty-search-results incident on `/docs/@v2.34.x/...`. `release.published` is the right cue: it fires exactly when a version becomes user-visible, not when its release branch is cut weeks earlier with possibly-incomplete docs. ## Coupling (important) This change is **intentionally inert until coder.com's `INDEXED_REFS_BY_CORPUS` allowlist becomes self-rotating** (filed under [DOCS-210](https://linear.app/codercom/issue/DOCS-210/automated-docs-index-lifecycle-management)). Until that lands, the handler still rejects new minors with `{action: "skipped", reason: "...not in INDEXED_REFS_BY_CORPUS"}` and this workflow logs the skip. Pre-wiring lets both halves land roughly in parallel so the next release cut after both ship is automatic. Reviewers: feel free to merge this independently. There is no downside to the wiring being live before the allowlist half ships; worst case, every release-publish event creates a no-op workflow run. ## Behavior trace (the cases the bash handles) <details> <summary>11 event scenarios I walked through by hand</summary> | Event | Tag | prerelease | Result | |---|---|---|---| | push to main | n/a | n/a | `index`, `ref=main` (existing) | | push to release/2.34 | n/a | n/a | `index`, `ref=release/2.34` (existing) | | workflow_dispatch index release/2.34 | n/a | n/a | `index`, `ref=release/2.34` (existing) | | workflow_dispatch delete release/2.31 | n/a | n/a | `delete`, `ref=release/2.31` (existing) | | release.published | `v2.35.0` | `false` | `index`, `ref=release/2.35` (new) | | release.published | `v2.35.0-rc.1` | `true` | notice + `exit 0` (new) | | release.published | `v2.35.0-rc.1` | `false` | notice + `exit 0`, regex miss (new) | | release.published | `v2.35` | `false` | notice + `exit 0`, regex miss (new) | | release.published | `release-2.35` | `false` | notice + `exit 0`, regex miss (new) | | release.published | `v0.0.0` | `false` | `index`, `ref=release/0.0` then handler rejects via allowlist (defense in depth) | | release.published | `` (empty) | unset | notice with `<unknown>` + `exit 0` | </details> ## Safety - The handler's allowlist gate still applies; this PR can only cause `{action: "skipped"}` responses until DOCS-210's allowlist-derivation lands. No risk of indexing an unintended ref. - The workflow's existing input validation (`case "$REF" in main|release/*)`) rejects any translation output that isn't `release/<int>.<int>`. Defense in depth in case the regex ever loosens by accident. - [DOCS-121](https://linear.app/codercom/issue/DOCS-121/post-mortem-docs-search-outage-2026-05-12-pr-25049-merge-wiped-docs) self-trigger risk is not present here: the new trigger is `release.published`, not push-on-paths. Workflow file edits cannot induce a release event. - `concurrency: { group: deploy-docs-${{ github.ref }} }` already exists. Release events have `github.ref=refs/tags/vX.Y.Z`, distinct from push events on the same release branch. A theoretical race resolves through the handler's atomic deleteBy+saveObjects. - The POST step now has an `if:` guard that skips downstream calls when the Compute step exits early without writing outputs. Closes the empty-env-var failure mode that coder-agents-review CRF-1 flagged. ## Verification - `actionlint .github/workflows/deploy-docs.yaml` clean. - `make pre-commit-light` clean: `fmt/shfmt`, `fmt/markdown`, `lint/actions/actionlint`, `lint/shellcheck`, `lint/markdown`, `lint/emdash`, `lint/typos`, etc. - `.github/workflows/test-deploy-docs-release.sh`: 14 cases pass (11 scenario table + 3 regex boundary cases). - Bash logic hand-traced through 11 event scenarios (table above). ## Out of scope - Build-time allowlist derivation in coder.com (DOCS-210a, will be filed/PR'd as a sibling change). - Webhook-driven cleanup of aged-out refs ([DOCS-210](https://linear.app/codercom/issue/DOCS-210) parent). - code-server release lifecycle (different repo, code-server's docs corpus stays at `main`). --- _Coder Agents on behalf of @nickvigilante._
533 lines
24 KiB
YAML
533 lines
24 KiB
YAML
name: Update coder.com/docs
|
|
|
|
# Triggers updates to the public docs at coder.com/docs from three
|
|
# sources:
|
|
#
|
|
# * push to main or release/* (docs/** only): markdown edits land in
|
|
# search and ISR within seconds.
|
|
# * release.published: when a stable vX.Y.Z release ships on this
|
|
# repo, the workflow translates the tag to its release/X.Y branch
|
|
# and reindexes. Eliminates the manual workflow_dispatch step from
|
|
# the mainline rotation. Prereleases and non-semver tags are
|
|
# skipped. See DOCS-327.
|
|
# * workflow_dispatch: operator-driven, with explicit action and ref.
|
|
#
|
|
# One preflight job (`changes`) feeds two parallel sibling jobs so that
|
|
# search records, the static cache, and any new routes register at the
|
|
# same time:
|
|
#
|
|
# 1. algolia-and-isr: HMAC-signed POST to coder.com/api/algolia-docs-sync.
|
|
# The handler re-extracts records for the (corpus, ref) pair and
|
|
# atomically replaces the slice of the Algolia `docs` index, then
|
|
# calls `res.revalidate(p)` for every navigable manifest entry to
|
|
# refresh Vercel's static-page cache without a full rebuild. Runs
|
|
# on every docs/** push.
|
|
#
|
|
# 2. vercel-rebuild: fires the Vercel deploy hook for a full
|
|
# build+deploy. Only runs when docs/manifest.json changed, since a
|
|
# manifest change can introduce or remove routes that Next.js's
|
|
# `getStaticPaths` only re-evaluates on a full rebuild.
|
|
#
|
|
# Markdown-only edits hit only path 1 and surface in seconds. Manifest
|
|
# edits hit both paths in parallel; the ISR revalidate is harmless
|
|
# against the previous deployment while the new build is in flight,
|
|
# and Vercel only swaps to the new build atomically when ready.
|
|
#
|
|
# https://vercel.com/docs/deploy-hooks#triggering-a-deploy-hook
|
|
# See coder/coder.com/src/pages/api/algolia-docs-sync.ts.
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- "release/*"
|
|
paths:
|
|
# Intentionally only docs/**. Edits to this workflow file must not
|
|
# auto-trigger a production reindex; use workflow_dispatch instead.
|
|
# See DOCS-121 (incident) and DOCS-124 (fix).
|
|
- "docs/**"
|
|
release:
|
|
# Fires when a draft release is published, when a release goes from
|
|
# prerelease to non-prerelease, or when a release is created already
|
|
# published. The Compute step below translates the published tag
|
|
# (vX.Y.Z) into its release/X.Y branch and skips prereleases. See
|
|
# DOCS-327 for the rotation context that motivated this trigger.
|
|
types: [published]
|
|
workflow_dispatch:
|
|
inputs:
|
|
action:
|
|
description: "Algolia action to perform"
|
|
required: true
|
|
type: choice
|
|
default: index
|
|
options:
|
|
- index
|
|
- delete
|
|
ref:
|
|
description: "Branch to (re)index or delete (e.g. main, release/2.32). Defaults to the workflow's checkout ref."
|
|
required: false
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# Do not cancel in-progress runs. Each run's `changes` job diffs the
|
|
# event's own (before, after) SHA pair, so two rapid pushes produce two
|
|
# non-overlapping surgical-mode requests. Cancelling the first run
|
|
# would silently drop its diff: the second run only sees its own pair,
|
|
# never sees the cancelled run's paths, and the dropped pages would
|
|
# stay stale until the next whole-branch reindex (manifest change,
|
|
# >50-file push, or manual workflow_dispatch). Runs are lightweight
|
|
# (shell + curl, ~2 minutes), so overlapping runs are cheap.
|
|
concurrency:
|
|
group: deploy-docs-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
# Detect what changed so the dependent jobs know:
|
|
# - whether a Vercel full rebuild is needed (manifest changed), and
|
|
# - which markdown pages to surgically reindex (the changed set).
|
|
#
|
|
# Outputs:
|
|
# manifest_changed: "true" | "false"
|
|
# paths_json: a JSON array of {path, status} objects, or "[]"
|
|
# when no markdown changes are eligible for
|
|
# surgical mode (manifest-only push, an
|
|
# uncomputable diff, a non-push event
|
|
# (workflow_dispatch or release.published),
|
|
# or a diff that exceeds the surgical-mode cap).
|
|
# An empty array tells the handler to fall back
|
|
# to whole-branch reindex.
|
|
changes:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
manifest_changed: ${{ steps.diff.outputs.manifest_changed }}
|
|
paths_json: ${{ steps.diff.outputs.paths_json }}
|
|
steps:
|
|
- name: Compute changed-files signal
|
|
id: diff
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
BEFORE_SHA: ${{ github.event.before }}
|
|
AFTER_SHA: ${{ github.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
emit_whole_branch_fallback() {
|
|
# Tells the algolia-and-isr job to operate in whole-branch
|
|
# mode by sending an empty paths array. The handler treats
|
|
# the absence of paths (or an empty list) as "reindex
|
|
# everything for this (corpus, ref)".
|
|
echo "paths_json=[]" >> "$GITHUB_OUTPUT"
|
|
}
|
|
# Non-push events (workflow_dispatch, release.published)
|
|
# have no diff range; treat as "manifest unchanged" so the
|
|
# manual or release-triggered reindex doesn't fire a Vercel
|
|
# rebuild it didn't ask for, and as whole-branch so the
|
|
# resulting reindex is exhaustive.
|
|
if [ "$EVENT_NAME" != "push" ]; then
|
|
echo "manifest_changed=false" >> "$GITHUB_OUTPUT"
|
|
emit_whole_branch_fallback
|
|
exit 0
|
|
fi
|
|
# First push to a brand-new branch has BEFORE_SHA = all zeros.
|
|
# In that edge case we conservatively assume the manifest is
|
|
# part of the initial state and trigger a full rebuild + a
|
|
# whole-branch reindex.
|
|
if [ -z "${BEFORE_SHA:-}" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
|
echo "manifest_changed=true" >> "$GITHUB_OUTPUT"
|
|
emit_whole_branch_fallback
|
|
exit 0
|
|
fi
|
|
# We don't need a full checkout for `git diff` against two
|
|
# known SHAs. A shallow fetch of just those two commits is
|
|
# enough.
|
|
git init -q
|
|
git remote add origin "https://github.com/${GITHUB_REPOSITORY}.git"
|
|
GIT_ERR=$(mktemp)
|
|
if ! git -c protocol.version=2 fetch --depth=1 origin "$BEFORE_SHA" "$AFTER_SHA" 2>"$GIT_ERR"; then
|
|
# Fall back to whole-branch if the shallow fetch failed
|
|
# (e.g. force-push rewrote history). Surfacing the git
|
|
# stderr line in the warning lets operators diagnose
|
|
# network or auth failures without reproducing the fetch
|
|
# manually.
|
|
FIRST_ERR=$(head -1 "$GIT_ERR" 2>/dev/null || true)
|
|
echo "::warning::Could not fetch BEFORE_SHA=$BEFORE_SHA: ${FIRST_ERR:-unknown}; assuming manifest changed"
|
|
echo "manifest_changed=true" >> "$GITHUB_OUTPUT"
|
|
emit_whole_branch_fallback
|
|
exit 0
|
|
fi
|
|
# Manifest signal.
|
|
if git diff --name-only "$BEFORE_SHA" "$AFTER_SHA" -- docs/manifest.json | grep -q .; then
|
|
echo "manifest_changed=true" >> "$GITHUB_OUTPUT"
|
|
# Manifest changes can rename or restructure routes, so
|
|
# surgical mode is not safe; a per-path delete keyed off
|
|
# the new canonical URL would miss records under old URLs.
|
|
# Whole-branch reindex is the right behavior here.
|
|
emit_whole_branch_fallback
|
|
exit 0
|
|
else
|
|
echo "manifest_changed=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
# Surgical mode: emit the changed markdown set as a JSON
|
|
# array of {path, status} objects. We use --name-status -z
|
|
# so the handler can distinguish modified/added (re-extract
|
|
# + save) from deleted/renamed-old-side (delete only), and
|
|
# so paths containing whitespace or quotes survive intact.
|
|
DIFF_FILE=$(mktemp)
|
|
git diff --name-status -z "$BEFORE_SHA" "$AFTER_SHA" -- 'docs/**/*.md' > "$DIFF_FILE"
|
|
# Parse the NUL-delimited diff into <path>\t<status> lines.
|
|
# `--name-status -z` uses NUL between fields and between
|
|
# records, with a special twist for renames: the record is
|
|
# `R<n>\0<old>\0<new>\0`, three NUL-delimited fields instead
|
|
# of two. Status codes: A=added, M=modified, T=type-changed
|
|
# (treated as modified), D=deleted, R<n>=renamed (we index
|
|
# the new path since that is the live route). Unknown codes
|
|
# log a warning and are skipped; a single awk handles both
|
|
# the parsing and the count so the two cannot disagree.
|
|
#
|
|
# Tested in test-deploy-docs-diff.sh. Keep that script in
|
|
# sync with any changes to this block.
|
|
PARSED=$(mktemp)
|
|
awk -v RS='\0' '
|
|
function emit(path, status) {
|
|
printf "%s\t%s\n", path, status
|
|
}
|
|
{
|
|
code = substr($0, 1, 1)
|
|
if (code == "A") { getline; emit($0, "added"); next }
|
|
if (code == "M") { getline; emit($0, "modified"); next }
|
|
if (code == "T") { getline; emit($0, "modified"); next }
|
|
if (code == "D") { getline; emit($0, "deleted"); next }
|
|
if (code == "R") {
|
|
# R<similarity>\0<old>\0<new>\0
|
|
getline old_path
|
|
getline new_path
|
|
emit(new_path, "renamed")
|
|
next
|
|
}
|
|
if ($0 != "") {
|
|
# Unknown status code. Consume the path field so the
|
|
# record alignment stays correct, then warn.
|
|
unknown_code = $0
|
|
getline unknown_path
|
|
printf "::warning::Unknown git diff status %s for %s; skipping.\n", unknown_code, unknown_path > "/dev/stderr"
|
|
}
|
|
}
|
|
' "$DIFF_FILE" > "$PARSED"
|
|
# Count is derived from the emitter output, so the count and
|
|
# the JSON payload cannot diverge by construction (DEREM-21).
|
|
CHANGED=$(wc -l < "$PARSED" | tr -d ' ')
|
|
if [ "$CHANGED" -eq 0 ]; then
|
|
# Markdown-only path filter on the trigger means we should
|
|
# only get here on edits to non-markdown files under docs/
|
|
# (e.g., images). Whole-branch reindex is overkill for
|
|
# those, but it is also harmless and avoids a special case;
|
|
# an empty paths array makes the handler skip both the
|
|
# save and the revalidate when no manifest entry maps to
|
|
# the changed file.
|
|
emit_whole_branch_fallback
|
|
exit 0
|
|
fi
|
|
# Cap at 50 changed files. Above that a whole-branch reindex
|
|
# is faster (one deleteBy + one saveObjects vs N deleteBy
|
|
# calls), and the surgical-mode payload also stays well under
|
|
# GitHub Actions' output size limit.
|
|
if [ "$CHANGED" -gt 50 ]; then
|
|
echo "::notice::$CHANGED markdown files changed; falling back to whole-branch reindex (cap is 50 for surgical mode)"
|
|
emit_whole_branch_fallback
|
|
exit 0
|
|
fi
|
|
# jq -Rcn slurps the <path>\t<status> lines and handles JSON
|
|
# escaping for quotes, backslashes, and any other special
|
|
# characters in the path.
|
|
PATHS_JSON=$(jq -Rcn '
|
|
[ inputs
|
|
| split("\t")
|
|
| { path: .[0], status: .[1] }
|
|
]
|
|
' < "$PARSED")
|
|
# Defense in depth: fail loudly if jq could not parse what
|
|
# we built. jq -c already validates structure; this catches
|
|
# the empty-stdin edge case.
|
|
if [ -z "$PATHS_JSON" ] || [ "$PATHS_JSON" = "null" ]; then
|
|
PATHS_JSON='[]'
|
|
fi
|
|
echo "paths_json=$PATHS_JSON" >> "$GITHUB_OUTPUT"
|
|
echo "Surgical mode: $CHANGED path(s) changed."
|
|
|
|
# Path 1: always run. Notifies coder.com to refresh Algolia records
|
|
# and ISR-revalidate the affected pages.
|
|
algolia-and-isr:
|
|
runs-on: ubuntu-latest
|
|
needs: changes
|
|
steps:
|
|
- name: Compute action and ref
|
|
id: input
|
|
env:
|
|
INPUT_ACTION: ${{ inputs.action }}
|
|
INPUT_REF: ${{ inputs.ref }}
|
|
GITHUB_REF_NAME: ${{ github.ref_name }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
|
|
run: |
|
|
set -euo pipefail
|
|
ACTION=""
|
|
REF=""
|
|
# release.published path: translate a stable vX.Y.Z tag into
|
|
# its release/X.Y branch and let the rest of the step
|
|
# validate. Skip prereleases and any tag that does not match
|
|
# the plain semver shape; backports (vX.Y.<patch>) are
|
|
# in-scope because they may carry doc updates worth
|
|
# reindexing. See DOCS-327. The handler's allowlist gates the
|
|
# downstream POST, so an unsupported minor still no-ops
|
|
# rather than reindexing something we did not intend.
|
|
#
|
|
# Tested in test-deploy-docs-release.sh. Keep that script in
|
|
# sync with any changes to this block.
|
|
if [ "${EVENT_NAME:-}" = "release" ]; then
|
|
if [ "${RELEASE_PRERELEASE:-false}" = "true" ]; then
|
|
echo "::notice::Skipping prerelease ${RELEASE_TAG:-<unknown>}; no docs reindex."
|
|
exit 0
|
|
fi
|
|
if [[ "${RELEASE_TAG:-}" =~ ^v([0-9]+)\.([0-9]+)\.[0-9]+$ ]]; then
|
|
ACTION="index"
|
|
REF="release/${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
|
|
echo "::notice::Release ${RELEASE_TAG} resolved to ref ${REF}."
|
|
else
|
|
echo "::notice::Skipping ${RELEASE_TAG:-<unknown>}: not a plain vX.Y.Z release tag."
|
|
exit 0
|
|
fi
|
|
fi
|
|
ACTION="${ACTION:-${INPUT_ACTION:-index}}"
|
|
REF="${REF:-${INPUT_REF:-$GITHUB_REF_NAME}}"
|
|
# Reject newlines/carriage returns in either input. GitHub
|
|
# Actions parses GITHUB_OUTPUT line-by-line with last-writer-
|
|
# wins, so a newline in $REF would let an operator dispatch
|
|
# `release/x\naction=delete\nref=main` past the validation
|
|
# below (the case `*` glob matches the multi-line string),
|
|
# then have `echo "ref=$REF" >> $GITHUB_OUTPUT` write three
|
|
# lines whose effective outputs are `action=delete ref=main`.
|
|
# `inputs.ref` is a single-line UI field; the REST API will
|
|
# accept anything. Reject embedded newlines explicitly.
|
|
case "$ACTION" in
|
|
*[$'\n\r']*)
|
|
echo "::error::action must not contain newlines."
|
|
exit 1
|
|
;;
|
|
esac
|
|
case "$REF" in
|
|
*[$'\n\r']*)
|
|
echo "::error::ref must not contain newlines."
|
|
exit 1
|
|
;;
|
|
esac
|
|
# The workflow_dispatch `type: choice` is enforced only by
|
|
# the GitHub UI. The REST API will accept any string. We
|
|
# validate explicitly so a malformed action never reaches
|
|
# the handler (which trusts this value after HMAC check).
|
|
case "$ACTION" in
|
|
index|delete) ;;
|
|
*)
|
|
echo "::error::Unsupported action '$ACTION'. Must be 'index' or 'delete'."
|
|
exit 1
|
|
;;
|
|
esac
|
|
case "$REF" in
|
|
main|release/*) ;;
|
|
*)
|
|
echo "::error::Unsupported ref '$REF'. Only main and release/* are eligible."
|
|
exit 1
|
|
;;
|
|
esac
|
|
# Refuse to run `action=delete` against main. The dispatch
|
|
# UI defaults `ref` to the dispatching branch (typically
|
|
# `main`), so a single forgotten field when cleaning up a
|
|
# release branch would wipe production search records.
|
|
# Force the operator to type the ref explicitly for delete.
|
|
if [ "$ACTION" = "delete" ] && [ "$REF" = "main" ]; then
|
|
echo "::error::Refusing to delete records for ref=main. Specify a release/* ref explicitly when dispatching delete."
|
|
exit 1
|
|
fi
|
|
echo "action=$ACTION" >> "$GITHUB_OUTPUT"
|
|
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: POST to coder.com docs indexer
|
|
# Sentinel guard. The Compute step has two release-event
|
|
# early-exit paths (prerelease skip, non-semver tag skip) that
|
|
# succeed without writing action/ref to GITHUB_OUTPUT. Without
|
|
# this guard, the POST would still fire with empty ACTION and
|
|
# REF env vars, sending stray no-op traffic to the production
|
|
# handler. The step only writes `action` on the success path,
|
|
# so its presence is a reliable proceed signal. See DOCS-327.
|
|
if: steps.input.outputs.action != ''
|
|
env:
|
|
ACTION: ${{ steps.input.outputs.action }}
|
|
REF: ${{ steps.input.outputs.ref }}
|
|
PATHS_JSON: ${{ needs.changes.outputs.paths_json }}
|
|
SECRET: ${{ secrets.ALGOLIA_DOCS_SYNC_SECRET }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${SECRET:-}" ]; then
|
|
echo "::error::ALGOLIA_DOCS_SYNC_SECRET is not configured."
|
|
exit 1
|
|
fi
|
|
# Build the webhook body. paths_json is always a valid JSON
|
|
# array (possibly empty) thanks to the changes job. An empty
|
|
# array tells the handler to do a whole-branch reindex; a
|
|
# non-empty array triggers surgical per-page mode.
|
|
if [ -z "${PATHS_JSON:-}" ]; then
|
|
PATHS_JSON='[]'
|
|
fi
|
|
BODY=$(jq -nc \
|
|
--arg action "$ACTION" \
|
|
--arg corpus "v2" \
|
|
--arg ref "$REF" \
|
|
--argjson paths "$PATHS_JSON" \
|
|
'{action: $action, corpus: $corpus, ref: $ref, paths: $paths}')
|
|
# SHA-256 HMAC over the exact bytes we POST. The handler verifies
|
|
# with crypto.timingSafeEqual on the same raw body, so the
|
|
# prefix and hex casing must match.
|
|
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')"
|
|
PATHS_COUNT=$(printf '%s' "$PATHS_JSON" | jq 'length')
|
|
MODE="whole-branch"
|
|
if [ "$PATHS_COUNT" -gt 0 ]; then
|
|
MODE="surgical ($PATHS_COUNT path(s))"
|
|
fi
|
|
echo "Action: $ACTION Ref: $REF Mode: $MODE"
|
|
RESPONSE=$(mktemp)
|
|
RC=0
|
|
HTTP_STATUS=$(curl --fail-with-body -sS \
|
|
--connect-timeout 10 \
|
|
--max-time 120 \
|
|
-o "$RESPONSE" \
|
|
-w '%{http_code}' \
|
|
-X POST \
|
|
-H 'Content-Type: application/json' \
|
|
-H "X-Coder-Signature: $SIG" \
|
|
--data "$BODY" \
|
|
https://coder.com/api/algolia-docs-sync) || RC=$?
|
|
# Render only an allowlisted subset of the handler response in
|
|
# the step summary. The handler can include free-form fields
|
|
# (error, reason, revalidateSampleErrors, skippedReasons,
|
|
# recordsByType) that may reflect upstream error strings. This
|
|
# repository is public, so the step summary is visible to
|
|
# anyone with read access; filter those fields out before the
|
|
# summary is written. The full response remains in the curl
|
|
# output captured in the workflow logs, which are restricted
|
|
# to repo collaborators.
|
|
#
|
|
# Keep this allowlist in sync with SyncResponseBody in
|
|
# coder/coder.com/src/pages/api/algolia-docs-sync.ts; add a
|
|
# field here only after confirming it is bounded enough to be
|
|
# safe for a public UI.
|
|
SAFE_RESPONSE=$(jq '
|
|
if type == "object" then
|
|
{
|
|
action,
|
|
corpus,
|
|
ref,
|
|
records,
|
|
pagesIndexed,
|
|
pagesSkipped,
|
|
revalidated,
|
|
revalidateFailed,
|
|
mode,
|
|
pathsRequested,
|
|
pathsSkipped,
|
|
index,
|
|
tookMs
|
|
} | with_entries(select(.value != null))
|
|
else
|
|
{}
|
|
end
|
|
' "$RESPONSE" 2>/dev/null) || SAFE_RESPONSE='{}'
|
|
{
|
|
echo "## Algolia + ISR sync"
|
|
echo
|
|
echo "- Action: \`$ACTION\`"
|
|
echo "- Ref: \`$REF\`"
|
|
echo "- Mode: \`$MODE\`"
|
|
echo "- HTTP status: \`${HTTP_STATUS:-n/a}\`"
|
|
echo
|
|
echo "### Response (allowlisted fields)"
|
|
echo
|
|
echo '```json'
|
|
printf '%s\n' "$SAFE_RESPONSE"
|
|
echo '```'
|
|
if [ "$RC" -ne 0 ]; then
|
|
echo
|
|
echo "### Error"
|
|
echo
|
|
echo "The request failed. See the workflow logs for the full handler response; the step summary suppresses free-form error strings because this repository is public."
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
if [ "$RC" -ne 0 ]; then
|
|
exit "$RC"
|
|
fi
|
|
|
|
# Path 2: full Vercel rebuild. Only fires when docs/manifest.json
|
|
# changed, because manifest changes can introduce or remove routes
|
|
# that Next.js's `getStaticPaths` only re-evaluates on a full build.
|
|
# Markdown-only edits don't need this; ISR revalidate covers them.
|
|
vercel-rebuild:
|
|
runs-on: ubuntu-latest
|
|
needs: changes
|
|
if: needs.changes.outputs.manifest_changed == 'true'
|
|
steps:
|
|
- name: Trigger Vercel deploy hook
|
|
env:
|
|
HOOK: ${{ secrets.DEPLOY_DOCS_VERCEL_WEBHOOK }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${HOOK:-}" ]; then
|
|
echo "::error::DEPLOY_DOCS_VERCEL_WEBHOOK is not configured."
|
|
exit 1
|
|
fi
|
|
# Mirror the sibling job's pattern: capture response body and
|
|
# HTTP status, write the step summary unconditionally, then
|
|
# propagate failure. Without this, set -e would kill the
|
|
# script before the summary block on curl failure.
|
|
RESPONSE=$(mktemp)
|
|
RC=0
|
|
HTTP_STATUS=$(curl --fail-with-body -sS \
|
|
--connect-timeout 10 \
|
|
--max-time 120 \
|
|
-o "$RESPONSE" \
|
|
-w '%{http_code}' \
|
|
-X POST "$HOOK") || RC=$?
|
|
# Render only an allowlisted subset of the Vercel deploy hook
|
|
# response (job.id, job.state, job.createdAt). The deploy hook
|
|
# URL itself is the only secret in this flow; the response
|
|
# shape is bounded today, but we filter explicitly to insulate
|
|
# the public step summary from any future shape change
|
|
# upstream and to keep the two summary blocks consistent.
|
|
SAFE_RESPONSE=$(jq '
|
|
if type == "object" and (.job | type) == "object" then
|
|
{ job: (.job | { id, state, createdAt } | with_entries(select(.value != null))) }
|
|
else
|
|
{}
|
|
end
|
|
' "$RESPONSE" 2>/dev/null) || SAFE_RESPONSE='{}'
|
|
{
|
|
echo "## Vercel rebuild"
|
|
echo
|
|
echo "- Reason: \`docs/manifest.json\` changed"
|
|
echo "- HTTP status: \`${HTTP_STATUS:-n/a}\`"
|
|
echo
|
|
echo "### Response (allowlisted fields)"
|
|
echo
|
|
echo '```json'
|
|
printf '%s\n' "$SAFE_RESPONSE"
|
|
echo '```'
|
|
if [ "$RC" -ne 0 ]; then
|
|
echo
|
|
echo "### Error"
|
|
echo
|
|
echo "The request failed. See the workflow logs for the full hook response; the step summary suppresses free-form error strings because this repository is public."
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
if [ "$RC" -ne 0 ]; then
|
|
exit "$RC"
|
|
fi
|