Skip to content

feat(.github/workflows): trigger Algolia, ISR, and Vercel deploy on docs/** changes#25049

Merged
nickvigilante merged 6 commits into
mainfrom
algolia-docs-index-workflow
May 12, 2026
Merged

feat(.github/workflows): trigger Algolia, ISR, and Vercel deploy on docs/** changes#25049
nickvigilante merged 6 commits into
mainfrom
algolia-docs-index-workflow

Conversation

@nickvigilante
Copy link
Copy Markdown
Contributor

@nickvigilante nickvigilante commented May 7, 2026

Folds the Algolia/ISR sync trigger and surgical-reindex path computation into the existing deploy-docs.yaml workflow so a single docs/** push fires every update path the docs site needs.

One preflight job feeds two parallel sibling jobs:

  • changes (preflight): diffs github.event.before against github.sha to compute manifest_changed and paths_json (a JSON array of {path, status} objects derived from git diff --name-status -z, capped at 50 entries). The mapping is A → added, M/T → modified, D → deleted, R<n> → renamed (indexed by the new path). Falls back to whole-branch (emits paths_json: "[]") on workflow_dispatch, the first push to a new branch, fetch failure, manifest changes (route restructuring would orphan records), or >50 markdown files.
  • algolia-and-isr (always, parallel with vercel-rebuild): HMAC-signed POST to coder.com/api/algolia-docs-sync with the paths_json array as part of the body. Refreshes the Algolia docs slice for the (corpus, ref) pair and ISR-revalidates every navigable route the handler touched. Markdown-only edits surface in seconds with no full rebuild. The step summary line Mode: \surgical` (N path(s))` lets operators verify which path ran without scrolling through the curl output.
  • vercel-rebuild (parallel with algolia-and-isr, only when docs/manifest.json changed): fires the existing Vercel deploy hook for a full build. Manifest changes can register or remove routes that Next.js's getStaticPaths only re-evaluates on a full build, so ISR-per-existing-path is not enough.

Trigger expanded from "main + manifest.json" to "main and release/* + any docs/**" so release-branch docs edits also flow through the same pipeline. The Vercel rebuild path stays gated on manifest changes regardless of branch.

The pure shell + curl + openssl + jq + awk pipeline is preserved verbatim. Zero Algolia or Node dependencies in CI.

Why one workflow instead of two

The original split (a standalone Algolia workflow + the existing deploy-docs.yaml) would have run twice per manifest push, with two parallel concurrency groups, two GitHub Actions step summaries, and two ways to forget to add a secret. Folding into one file makes the trigger story symmetrical: "docs change → all docs surfaces refresh," with the rebuild path being a strict superset of the ISR path, and the surgical path strictly cheaper than whole-branch when computable.

Pre-merge testing

The companion handler PR (coder/coder.com#741) supports an ALGOLIA_DOCS_INDEX env-var override, scoped to docs_smoke on the Vercel preview deploy, so this workflow can be exercised end-to-end against a disposable index without touching production records. The smoke harness at ~/audit/smoke/run.sh (workspace-only) signs and posts the same body shape this workflow does, so it covers the same crypto path. To exercise the workflow itself, push a docs-only commit to a throwaway branch and watch the step summary; the algolia-and-isr job will print the resolved mode.

Prerequisites before this can do anything useful

  1. secrets.ALGOLIA_DOCS_SYNC_SECRET must be added as an Actions secret on this repo. The same value goes on coder.com's Vercel env. The workflow logs a clear error and aborts with no network call if the secret is missing.
  2. The handler at coder/coder.com#741 must be merged and deployed. Without it, the POST will 404.
  3. secrets.DEPLOY_DOCS_VERCEL_WEBHOOK is already in place from the existing deploy-docs.yaml; this PR does not change its usage.

Demo, validation, and design

  • Front-end-only fixes (modal layout, scroll-shadow, rank-order preservation): coder/coder.com#749 ships these against production today, independent of this PR.
  • Companion handler PR on coder.com: coder/coder.com#741. Includes the surgical-mode plumbing this workflow's paths_json output drives.
  • Full design lives in the workspace at ~/plans/algolia-search-revamp.md. Key sections:
    • §6.0–6.2: why the indexer lives in coder.com, not here.
    • §6.7: per-version add/remove mechanics.
    • §6.8: ISR revalidate rationale and same-time refresh.
    • §6.9: surgical per-page reindex (workflow + handler + planning rules).

This PR was generated by Coder Agents.

@nickvigilante nickvigilante changed the title feat(.github/workflows): trigger Algolia docs reindex on docs/** changes feat(.github/workflows): trigger Algolia, ISR, and Vercel deploy on docs/** changes May 7, 2026
Adds a tiny shell-only GitHub Actions workflow that POSTs an HMAC-signed
request to the new coder.com indexer at /api/algolia-docs-sync whenever
docs/** content changes on main or a release/* branch.

The workflow:

  - Has zero Algolia or Node.js dependencies. Just curl + openssl + jq.
  - Computes SHA-256 HMAC over the JSON body using
    secrets.ALGOLIA_DOCS_SYNC_SECRET (shared with coder.com).
  - Validates that the ref is main or release/* and aborts otherwise.
  - Supports a workflow_dispatch action input ('index' | 'delete')
    so a maintainer can backfill any version or clean up records for a
    dropped version without round-tripping through coder.com's repo.
  - Writes the handler response to the workflow run summary so we can
    audit what got indexed without leaving GitHub.

The Algolia admin key never lives in this repo. Only the shared HMAC
secret does, which limits blast radius if a runner ever leaks a
secret.

This change does NOT touch any docs content. The indexing handler in
coder.com lands separately at coder/coder.com#TBD; see the design doc
section 6 (architecture) for the full story.

Prerequisite: ALGOLIA_DOCS_SYNC_SECRET must be added as an Actions
secret on this repo (and the same value on coder.com's Vercel env)
before merge. The workflow logs a clear error and aborts with no
network call if the secret is missing.

This change was authored by a Coder agent on behalf of @nickvigilante.
Folds the standalone algolia-docs-index.yaml workflow into the
existing deploy-docs.yaml so a single docs/** push fires every
required update path:

  - algolia-and-isr (always): HMAC-signed POST to
    coder.com/api/algolia-docs-sync. Refreshes the Algolia docs
    slice for the (corpus, ref) pair AND ISR-revalidates every
    navigable manifest entry. Markdown-only edits surface in
    seconds with no full rebuild.

  - vercel-rebuild (only when docs/manifest.json changed): fires
    the Vercel deploy hook. Manifest changes can register new
    routes that Next.js's getStaticPaths only re-evaluates on a
    full build, so ISR-per-existing-path is not enough.

A small "changes" preflight job computes the diff between
github.event.before and github.sha to set a single
manifest_changed output. workflow_dispatch invocations bypass
the diff and skip the Vercel rebuild path so manual reindex/
delete actions don't trigger an unnecessary deploy.

Trigger expanded from "main + manifest.json" to "main and
release/* + any docs/**" so release branch docs edits also
flow through the same pipeline. The Vercel rebuild path stays
gated on manifest changes regardless of branch.

This change was generated by Coder Agents.
… reindex

The deploy-docs workflow's 'changes' job now diffs the push's BEFORE_SHA
and AFTER_SHA, walks the result with awk to map git status codes
(A/M/D/R/T) onto the handler's PathStatus enum, and emits a JSON array
on the 'paths_json' output. The algolia-and-isr job forwards the array
to coder.com's /api/algolia-docs-sync via the existing HMAC-signed
webhook body.

When paths_json is non-empty, the handler runs in surgical mode and
only re-extracts the changed pages. The path is the safe fallback for:
- workflow_dispatch (no diff range available)
- the very first push to a new branch (zero BEFORE_SHA)
- a fetch failure (e.g. force-push rewrote history)
- a manifest.json change (route restructuring; surgical isn't safe)
- > 50 changed markdown files (faster as one whole-branch reindex)
- any non-markdown change under docs/

The workflow logs the resolved mode in the curl step and adds a 'Mode:
`surgical` (N path(s))' line to the GitHub Actions step summary so
operators can verify which path ran.

Refs: algolia-search-revamp.md \u00a76.9 (to be added)
@nickvigilante nickvigilante force-pushed the algolia-docs-index-workflow branch from e4a9820 to 2b5b2cf Compare May 11, 2026 17:27
@nickvigilante nickvigilante marked this pull request as ready for review May 11, 2026 17:27
@nickvigilante
Copy link
Copy Markdown
Contributor Author

/coder-agents-review

Copy link
Copy Markdown
Contributor

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-designed workflow with thorough edge-case coverage. The fallback hierarchy (workflow_dispatch, zero SHA, fetch failure, manifest change, >50 files, non-markdown files) correctly defaults to whole-branch reindex at every ambiguous state. Expression injection is fully mitigated: all GitHub context values flow through env: blocks into shell variables, never interpolated inline in run:. The inline documentation is good enough that an operator reading this file cold will understand every branch point.

Three P2, six P3, six Nit. The sharpest finding is DEREM-4: cancel-in-progress: true combined with surgical mode silently drops Algolia updates when a push is cancelled, and there is no catch-up mechanism. The fix is one word.

"That is a doctor saying 'something went wrong; we did the safe thing.' The git stderr [...] is the actual diagnosis, and it got piped into the void." (Leorio, on the suppressed fetch stderr)

🤖 This review was automatically generated with Coder Agents.

Comment thread .github/workflows/deploy-docs.yaml Outdated
Comment thread .github/workflows/deploy-docs.yaml
Comment thread .github/workflows/deploy-docs.yaml Outdated
Comment on lines +147 to +199
# GitHub Actions' output size limit.
CHANGED=$(wc -l < "$DIFF_FILE")
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_full_branch_fallback
exit 0
fi
if [ "$CHANGED" -gt 50 ]; then
echo "::notice::$CHANGED markdown files changed; falling back to whole-branch reindex (cap is 50 for surgical mode)"
emit_full_branch_fallback
exit 0
fi
# Map each `git diff --name-status` line to a {path, status}
# JSON object. Statuses: A=added, M=modified, D=deleted,
# R<n>=renamed (with similarity score, e.g. R100). We
# collapse R into "renamed" so the handler can treat both
# sides of a rename uniformly.
PATHS_JSON=$(awk '
BEGIN { print "[" }
{
code = substr($1, 1, 1)
status = "modified"
if (code == "A") status = "added"
else if (code == "M") status = "modified"
else if (code == "D") status = "deleted"
else if (code == "R") status = "renamed"
else if (code == "T") status = "modified"
else next
# For renames the second field is the new path; the
# first path field is the old path. We index by the new
# path for renames since that is the live route. The old
# path will be cleaned up by the manifest walk if the
# rename also touched manifest.json (handled above) or
# via natural record expiry on the next whole-branch
# reindex. We do not attempt to delete-by-old-canonical
# in surgical mode because the canonical URL of the old
# path is no longer reachable through the manifest.
if (code == "R") path = $3
else path = $2
if (NR > 1) printf ","
printf "{\"path\":\"%s\",\"status\":\"%s\"}", path, status
}
END { print "]" }
' "$DIFF_FILE")
# Single-line, no whitespace: jq normalizes for the webhook
# body later. Fail loudly if jq cannot parse what we built.
PATHS_JSON=$(printf '%s' "$PATHS_JSON" | jq -c .)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [DEREM-6] No test for the changes job's shell logic; the project has an established pattern for exactly this. (Bisky)

"The changes job is ~100 lines of branching shell with at least 7 distinct exit paths. The awk block alone has 6 code paths. None of this is tested."

The project already has test-docs-preview-mapper.sh (89 lines), which extracts a workflow's shell logic into a standalone function and exercises every branch with assertions. This PR introduces more complex logic and ships zero tests. DEREM-3 (the NR counter bug) is a concrete instance a test would have caught.

🤖

Copy link
Copy Markdown
Contributor Author

@nickvigilante nickvigilante May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushing back on this one.

test-docs-preview-mapper.sh tests a pure string-mapping function over arrays. That is straightforwardly testable in shell. The changes job here depends on real git state: git init + remote add + shallow fetch of two SHAs + git diff --name-status -z against those SHAs. Mocking that out in shell tests isn't a small refactor, and the codebase has no precedent for spinning up a git repo as a test fixture.

I exercised the rewritten awk manually with synthetic NUL-delimited inputs covering A, M, T, D, and R codes, plus paths containing spaces, double quotes, and backslashes. All survive correctly through to a well-formed webhook body. I would rather land that empirical validation now and add a proper test harness if production surprises us, vs blocking the cutover on a testability refactor that has no precedent in this repo.

Happy to revisit if you disagree.

Coder Agents on behalf of @nickvigilante.

Comment thread .github/workflows/deploy-docs.yaml Outdated
Comment thread .github/workflows/deploy-docs.yaml Outdated
Comment thread .github/workflows/deploy-docs.yaml Outdated
Comment thread .github/workflows/deploy-docs.yaml Outdated
Comment thread .github/workflows/deploy-docs.yaml Outdated
Comment thread .github/workflows/deploy-docs.yaml
Comment thread .github/workflows/deploy-docs.yaml Outdated
Addresses 14 findings from the agent review on PR #25049:

- DEREM-4 (P2): drop `cancel-in-progress: true`. Two rapid pushes would
  silently lose the first push's surgical-mode diff; runs are cheap so
  let them overlap.
- DEREM-5 (P2): refuse workflow_dispatch `action=delete` when ref
  resolves to `main`. The dropdown defaults `ref` to the dispatching
  branch, so a forgotten field could wipe production search records.
- DEREM-7 (P3): capture `git fetch` stderr and surface the first line
  in the warning so operators can diagnose without reproducing the
  fetch.
- DEREM-8 (P3): vercel-rebuild now mirrors algolia-and-isr's pattern.
  Step summary is written unconditionally, then RC is propagated.
- DEREM-9 (P3): add `--connect-timeout 10 --max-time 120` to both curl
  invocations so an unresponsive endpoint fails in ~2 minutes instead
  of the 6h runner default.
- DEREM-10 (P3): validate `action` server-side. `type: choice` is
  UI-only; the REST API will accept any string.
- DEREM-3 (P3) + DEREM-2 (P3): rewrite the `changes` job's awk to use
  NUL-delimited input (`git diff --name-status -z`) and a stateful
  parser, then pipe to `jq -Rcn` for JSON escaping. The previous
  approach had two latent bugs: (1) `NR > 1` would have emitted a
  leading comma if the first record was skipped, and (2) paths with
  spaces, quotes, or backslashes could break awk's default field
  splitting or produce invalid JSON.
- DEREM-1, -11, -12, -13, -14, -15 (Nit): replace `--` punctuation in
  comment, rewrite description note about parallel jobs, fix the
  rename-field comment (third field is the new path), align trigger
  pattern with `release/*` convention, rename
  `emit_full_branch_fallback` to `emit_whole_branch_fallback` to match
  the prose, uppercase locals (`RESPONSE`, `RC`, `HTTP_STATUS`).

DEREM-6 (P2: no tests for the changes job) deferred. The shell logic
depends on real git state and the codebase has no precedent for
running git inside a test fixture. Leaving as a follow-up if surprises
emerge.

Verified end-to-end with synthetic NUL-delimited inputs covering A, M,
T, D, and R<n> codes, plus paths containing spaces, double quotes, and
backslashes. All survive through the awk-to-jq pipeline correctly.
Copy link
Copy Markdown
Contributor Author

nickvigilante commented May 12, 2026

Pushed 0dd0a5b addressing 14 of the 15 findings. Summary:

Addressed

P2

  • DEREM-4 (cancel-in-progress: true drops surgical updates): set to false. Added a block comment explaining why: each run diffs its own (before, after) pair, so cancelling drops paths that no later run will ever see.
  • DEREM-5 (workflow_dispatch delete + blank ref wipes main): explicit refuse-delete-against-main check after the input parsing. Operator must type a release/* ref explicitly for delete.

P3

  • DEREM-2 (path escaping) + DEREM-3 (NR counter bug): rewrote the awk to use --name-status -z (NUL-delimited input), a stateful per-record parser, and pipe through jq -Rcn for JSON escaping. The previous design had two latent issues: paths with spaces/quotes/backslashes could break awk's default field splitting or produce invalid JSON, and NR > 1 would have emitted a leading comma if the first record was skipped via next. Verified end-to-end with synthetic NUL-delimited inputs covering A, M, T, D, and R codes, plus paths containing spaces, double quotes, and backslashes.
  • DEREM-7 (suppressed git fetch stderr): capture stderr to $GIT_ERR, include the first line in the ::warning:: message so operators can diagnose without reproducing the fetch manually.
  • DEREM-8 (vercel-rebuild discards response on failure): mirrors the sibling job's pattern. Captures response body and HTTP status, writes step summary unconditionally, then propagates the curl exit code.
  • DEREM-9 (no curl timeouts): added --connect-timeout 10 --max-time 120 to both curl invocations. An unresponsive endpoint now fails the job in ~2 minutes instead of consuming the 6-hour runner default.
  • DEREM-10 (no shell-side action validation): added a case "$ACTION" in index|delete) ;; *) error ;; esac check. type: choice is UI-only; defense in depth for the REST API path.

Nit

  • DEREM-1 (-- punctuation): replaced with ;.
  • DEREM-11 (description says "three jobs in sequence"): rewrote the PR description's intro paragraph and the workflow's own header comment to say "one preflight job feeds two parallel sibling jobs."
  • DEREM-12 (rename-field comment): rewrote the comment to match the code, and the awk now uses named variables old_path and new_path instead of $2/$3 so the relationship is obvious.
  • DEREM-13 (release/**release/*): aligned with the codebase convention.
  • DEREM-14 (emit_full_branch_fallbackemit_whole_branch_fallback): renamed; matches the prose everywhere.
  • DEREM-15 (lowercase locals): renamed response, status, rc to RESPONSE, HTTP_STATUS, RC for consistency with the rest of the block.

Deferred: DEREM-6 (P2: no tests for the changes job)

Not fixing in this PR. The reviewer is right that there's precedent (test-docs-preview-mapper.sh), but that script tests a pure mapping function over arrays of strings. The changes job's logic depends on real git state (BEFORE_SHA resolved against a freshly-initialized repo on the runner), and the codebase has no precedent for running git inside a test fixture. Doing it right would require either extracting and mocking the git invocations (large refactor for a workflow file) or setting up a real repo as a test fixture (new pattern).

The behavior is now exercised manually (NUL-delimited synthetic inputs covering every status code and every problematic path shape, all passing through to a well-formed webhook body), and I'd rather land that empirical validation today and follow up with a proper test harness if the workflow surprises us in production. Happy to push back on this if you disagree.


Response posted by Coder Agents on behalf of @nickvigilante.

@nickvigilante
Copy link
Copy Markdown
Contributor Author

/coder-agents-review

Copy link
Copy Markdown
Contributor

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 of 15 R1 findings addressed in 0dd0a5b. All fixes verified by Netero and the panel. The awk rewrite (NUL-delimited input + jq for JSON escaping) is a clean structural improvement over the original.

One new P2, one new P3. The new P2 (DEREM-20) is sharp: the delete-main safety guard added for DEREM-5 is bypassable via GITHUB_OUTPUT newline injection in the ref input. The fix is two lines.

DEREM-6 (contested, P2): The panel evaluated the author's defense and found it conflates git-dependent branching (hard to test, defense accepted) with git-independent awk parsing (trivially testable with synthetic input, defense rejected). 3/4 reviewers support keeping the finding open. The author's manual verification proves the awk is testable without git; the test artifact was not preserved. DEREM-21 (counter/emitter divergence on unrecognized status codes) is a concrete sibling instance from the same class as DEREM-3. This needs a human decision: file a ticket for the test harness, or accept the gap.

"'Will add a test harness if production surprises' with no linked ticket is a drop, not a deferral." (Bisky)

🤖 This review was automatically generated with Coder Agents.

Comment thread .github/workflows/deploy-docs.yaml
Comment thread .github/workflows/deploy-docs.yaml Outdated
Comment thread .github/workflows/deploy-docs.yaml Outdated
DEREM-20 (P2): reject newlines and carriage returns in workflow_dispatch
`action` and `ref` inputs before any case validation. Without this, an
operator can dispatch a multi-line ref like `release/x\naction=delete\nref=main`
that passes the shell-glob case check (the trailing `*` matches across
newlines) and then writes multiple lines into $GITHUB_OUTPUT via
`echo "ref=$REF"`. GitHub Actions parses GITHUB_OUTPUT line-by-line
with last-writer-wins, so the effective outputs become
`action=delete ref=main`, bypassing the delete-against-main guard.

DEREM-21 (P3): collapse the duplicate awk parsers into a single parser
whose output is the source of truth for both the count and the JSON
payload. The old design had a counter awk with a permissive catch-all
and an emitter awk with no catch-all; an unrecognized status code
would consume the path on the counter side and misalign the emitter,
producing count=2/emit=1 on the reviewer's reproduction. The new
design has one awk that explicitly handles unknown codes (consume the
path, log a workflow warning, do not emit), and the count is derived
from `wc -l` on the emitter output. The two cannot diverge by
construction.

DEREM-6 (P2, was deferred R1): added test-deploy-docs-diff.sh that
exercises the awk parser against 22 synthetic NUL-delimited inputs
covering status codes (A/M/T/D/R<n>), path escaping (spaces, double
quotes, backslashes, tabs), unknown-code handling, count/emitter
agreement, and the 50-record boundary. The script mirrors the
existing test-docs-preview-mapper.sh precedent: a self-contained
shell next to the workflow with the parser copy-pasted from the
workflow. A workflow comment names the test script as the contract.
Copy link
Copy Markdown
Contributor Author

Pushed 257c755 addressing all three R2 findings.

Addressed

P2

  • DEREM-20 (newline injection in $GITHUB_OUTPUT): explicit case reject on $'\n\r' before any other validation, for both $ACTION and $REF. Verified against the exact attack payload from the review: release/x\naction=delete\nref=main now exits with ::error::ref must not contain newlines. before any output write or webhook call. Tested clean values (main, release/2.32) still accept; tested CR-only variant (release/x\rinjected) and action-side variant (index\naction=delete) reject as well.

  • DEREM-6 (re-raised, no test for the awk parser): conceded. The panel's argument was correct; I had conflated the git-dependent branching (genuinely hard to test) with the awk parsing (a pure text transformation with synthetic-input precedent already in the repo). Added .github/workflows/test-deploy-docs-diff.sh, mirroring test-docs-preview-mapper.sh structurally. 22 assertions across four sections covering status codes, path escaping (spaces/quotes/backslashes/tabs), the panel's exact unknown-code reproduction, count/emitter agreement, and the 50-record boundary. All pass locally. The workflow comment now names the test script as the contract for the parser.

P3

  • DEREM-21 (counter/emitter divergence on unknown codes): fixed structurally. Collapsed the two awks into one that explicitly handles unknown codes (consume the path, log ::warning::, do not emit), and derived the count from wc -l on the emitter output. Two parsers cannot diverge because there is only one parser; the count is a function of its output.

DEREM-6 follow-up note

The new test script is structurally identical to test-docs-preview-mapper.sh (which has been in the repo since the docs preview workflow shipped). That script is not currently wired into a CI step; it runs as local/documentation tooling. Wiring both scripts into a CI job is straightforward (single bash step in this same workflow file, or a sibling test-docs-workflows.yaml running on PRs that touch .github/workflows/{deploy-docs,docs-preview}.yaml). Happy to do that in a follow-up PR if the reviewer thinks the in-CI execution is the actual bar; the test artifact existing and matching the precedent script's structure was what I read as the explicit ask.


Response posted by Coder Agents on behalf of @nickvigilante.

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

1 similar comment
@nickvigilante
Copy link
Copy Markdown
Contributor Author

/coder-agents-review

Copy link
Copy Markdown
Contributor

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 22 findings across three rounds are resolved. The three R2 fixes in 257c755 are clean:

  • DEREM-20 (newline injection): newline/CR rejection fires before any GITHUB_OUTPUT write or pattern validation. Both user-controllable inputs checked. Root cause addressed.
  • DEREM-6 (no test): test-deploy-docs-diff.sh delivers 22 assertions across status codes, path escaping, unknown-code handling, count/emitter agreement, and 50-record boundary. All pass. The test follows the established test-docs-preview-mapper.sh pattern.
  • DEREM-21 (counter/emitter divergence): single awk replaces the two-parser design. Count derived from wc -l on emitter output. Divergence impossible by construction.

The panel verified all fixes. Bisky confirmed every assertion would fail if its target behavior changed. Mafuuu traced the newline check ordering and confirmed no sibling injection vectors. Meruem confirmed the single-awk structural fix.

"Twenty-two assertions, four custom assert helpers, NUL-delimited synthetic inputs fed through file descriptors to avoid bash's NUL-stripping. Somebody actually thought about this." (Bisky)

🤖 This review was automatically generated with Coder Agents.

Copy link
Copy Markdown
Contributor

@jdomeracki-coder jdomeracki-coder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Review: Approved (Coder Agents generated)

No critical or high-severity issues found. The workflow demonstrates strong security hygiene:

  • All untrusted inputs pass through env: context (no expression injection)
  • Secrets used only in HMAC computation / curl, never logged
  • Explicit newline/CR rejection before GITHUB_OUTPUT writes prevents multi-line override attacks
  • Whitelist validation for action and ref; delete-on-main refused
  • NUL-safe diff parsing with single awk for count+emission (no divergence possible)
  • set -euo pipefail, proper quoting, and mktemp throughout
  • Comprehensive regression tests for the parser

Two minor observations (non-blocking):

  1. Medium: Remote handler response bodies are written to the step summary. If the handler ever leaks sensitive data in errors, it would be visible to repo readers.
  2. Low: Consider branch protection rules restricting release/* push access to limit who can trigger reindexing.

… step summary

The Algolia + ISR sync and Vercel rebuild step summaries previously
dumped the full curl response body into $GITHUB_STEP_SUMMARY via
`cat "$RESPONSE"`. coder/coder is a public repository, so the step
summary is visible to anyone with read access. The handler response
includes free-form fields (error, reason, revalidateSampleErrors,
skippedReasons, recordsByType) that may reflect upstream error strings
(e.g. `manifest fetch failed: ${err.message}`, raw `res.revalidate(p)`
errors). The Vercel deploy hook response is bounded today but is a
third-party shape that could change without our involvement.

Pre-filter both response bodies with jq, allowlisting only fields whose
contents we have validated as safe for a public UI: action, corpus,
ref, records, pagesIndexed, pagesSkipped, revalidated, revalidateFailed,
mode, pathsRequested, pathsSkipped, index, tookMs for the Algolia
handler; job.id, job.state, job.createdAt for the Vercel hook. Add a
comment pointing at SyncResponseBody in coder/coder.com so the next
person who edits this file knows where the contract lives.

When curl returns non-zero, suppress the raw error string and instead
print an `### Error` block telling operators to inspect the workflow
logs (which are restricted to repo collaborators) for full details. The
HTTP status code remains in the summary regardless.

Addresses the medium-severity finding from the human security review on
PR #25049.
Copy link
Copy Markdown
Contributor Author

Thanks for the review, @jdomeracki-coder. Both findings handled:

Medium (response body in step summary): fixed in ece95e9

The Algolia sync and Vercel rebuild jobs no longer cat "$RESPONSE" into $GITHUB_STEP_SUMMARY. Both bodies are pre-filtered with jq and only an allowlisted subset is rendered.

For algolia-and-isr, the allowlist matches the bounded fields in SyncResponseBody (the handler's response type): action, corpus, ref, records, pagesIndexed, pagesSkipped, revalidated, revalidateFailed, mode, pathsRequested, pathsSkipped, index, tookMs. The free-form fields (error, reason, revalidateSampleErrors, skippedReasons, recordsByType) are dropped before the summary is written. A comment in the workflow points at SyncResponseBody so the next person editing the file knows where the contract lives.

For vercel-rebuild, the allowlist is .job.id, .job.state, .job.createdAt. The Vercel deploy hook response is bounded today but is a third-party shape; filtering explicitly insulates the public summary from any future change upstream.

When curl returns non-zero, the raw error string is suppressed and an ### Error block tells operators to look in the workflow logs (collaborator-restricted) for full details. The HTTP status code still renders. with_entries(select(.value != null)) strips fields the handler omitted, so a skipped response only shows the fields that actually populated.

Verified locally against:

  • a full success body with all unsafe fields populated -> only the 14 allowlisted fields render
  • a skipped response with several nulls -> only the 3 non-null safe fields render
  • malformed JSON -> {} (no crash, no leak)
  • a non-object root (["i","am","not","an","object"]) -> {} (no crash, no leak)
  • a Vercel response with webhookSecret and internalDeployUrl injected -> only job.id, job.state, job.createdAt render
  • a Vercel response with no job key (raw error shape) -> {} (no leak of the error string into the summary)

Low (branch protection for release/*): tracked as DOCS-120

Branch protection is a repo-admin setting rather than a workflow change, so I filed it as a follow-up in the same Linear project (Algolia search improvements) instead of bundling it here. The ticket covers auditing the current release/* policy, narrowing push access to the existing release-management group, documenting the allowlist in the docs release runbook, and verifying with a non-allowlisted account. T-shirt size XS once the policy decision is made.

Ready to merge once CI is green.


Coder Agents on behalf of @nickvigilante.

@nickvigilante nickvigilante merged commit 36d52ba into main May 12, 2026
28 of 29 checks passed
@nickvigilante nickvigilante deleted the algolia-docs-index-workflow branch May 12, 2026 18:18
@github-actions github-actions Bot locked and limited conversation to collaborators May 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants