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._
218 lines
7.8 KiB
Bash
Executable File
218 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Regression tests for the release.published branch in the "Compute
|
|
# action and ref" step of deploy-docs.yaml. The workflow translates a
|
|
# stable vX.Y.Z release tag into its release/X.Y branch and skips
|
|
# prereleases or non-semver tags. This script exercises that bash
|
|
# block against the documented event sources (push, workflow_dispatch,
|
|
# release.published) plus regex boundary cases so we can catch
|
|
# regressions in the regex, the prerelease gate, or either early-exit
|
|
# path without spinning up the full workflow.
|
|
#
|
|
# Keep compute_action_ref below in sync with deploy-docs.yaml. The
|
|
# workflow comment "Tested in test-deploy-docs-release.sh" is the
|
|
# contract.
|
|
|
|
set -euo pipefail
|
|
|
|
# compute_action_ref runs the workflow's release-event logic in a
|
|
# subshell so its `exit 0` only ends one invocation. Reads EVENT_NAME,
|
|
# RELEASE_TAG, RELEASE_PRERELEASE, INPUT_ACTION, INPUT_REF, and
|
|
# GITHUB_REF_NAME from the environment and prints lines compatible
|
|
# with the tests below:
|
|
# * release skip: stdout has the `::notice::` line, no ACTION/REF.
|
|
# * release accept: stdout has ACTION=, REF=, and the `::notice::`
|
|
# line, in the same order as the workflow.
|
|
# * push/workflow_dispatch: stdout has ACTION= and REF= only.
|
|
#
|
|
# This duplicates the workflow block byte-for-byte. Update both
|
|
# together; the assertions below describe the contract.
|
|
compute_action_ref() {
|
|
(
|
|
set -u
|
|
ACTION=""
|
|
REF=""
|
|
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}}"
|
|
echo "ACTION=$ACTION"
|
|
echo "REF=$REF"
|
|
)
|
|
}
|
|
|
|
failures=0
|
|
section=""
|
|
|
|
start_section() {
|
|
section="$1"
|
|
echo
|
|
echo "--- $section ---"
|
|
}
|
|
|
|
# run_case clears the relevant env vars and runs the function with the
|
|
# values from the scenario. Captures stdout into a string the test can
|
|
# assert against. Unset vars use the function's :- defaults so the
|
|
# tests exercise the same fallbacks the workflow does.
|
|
run_case() {
|
|
local event_name="$1"
|
|
local release_tag="$2"
|
|
local release_prerelease="$3"
|
|
local input_action="$4"
|
|
local input_ref="$5"
|
|
local github_ref_name="$6"
|
|
EVENT_NAME="$event_name" \
|
|
RELEASE_TAG="$release_tag" \
|
|
RELEASE_PRERELEASE="$release_prerelease" \
|
|
INPUT_ACTION="$input_action" \
|
|
INPUT_REF="$input_ref" \
|
|
GITHUB_REF_NAME="$github_ref_name" \
|
|
compute_action_ref
|
|
}
|
|
|
|
# assert_equals checks the captured output against the expected lines
|
|
# joined by literal newlines. Quoting prevents shell expansion of `*`
|
|
# or `$` inside the expected payload.
|
|
assert_equals() {
|
|
local description="$1"
|
|
local actual="$2"
|
|
local expected="$3"
|
|
if [ "$actual" = "$expected" ]; then
|
|
printf 'ok %s\n' "$description"
|
|
else
|
|
printf 'FAIL %s\n' "$description"
|
|
printf ' expected:\n'
|
|
printf '%s\n' "$expected" | sed 's/^/ /'
|
|
printf ' actual:\n'
|
|
printf '%s\n' "$actual" | sed 's/^/ /'
|
|
failures=$((failures + 1))
|
|
fi
|
|
}
|
|
|
|
# Each scenario names its event source so a future reader can match a
|
|
# test to the workflow path it exercises without reading the bash.
|
|
|
|
# ---------------------------------------------------------------
|
|
start_section "push event (existing behavior)"
|
|
# ---------------------------------------------------------------
|
|
|
|
actual=$(run_case "push" "" "" "" "" "main")
|
|
assert_equals "push to main keeps ACTION=index, REF=main" \
|
|
"$actual" \
|
|
$'ACTION=index\nREF=main'
|
|
|
|
actual=$(run_case "push" "" "" "" "" "release/2.34")
|
|
assert_equals "push to release/2.34 keeps ACTION=index, REF=release/2.34" \
|
|
"$actual" \
|
|
$'ACTION=index\nREF=release/2.34'
|
|
|
|
# ---------------------------------------------------------------
|
|
start_section "workflow_dispatch event (existing behavior)"
|
|
# ---------------------------------------------------------------
|
|
|
|
actual=$(run_case "workflow_dispatch" "" "" "index" "release/2.34" "main")
|
|
assert_equals "workflow_dispatch index release/2.34 honors inputs" \
|
|
"$actual" \
|
|
$'ACTION=index\nREF=release/2.34'
|
|
|
|
actual=$(run_case "workflow_dispatch" "" "" "delete" "release/2.31" "main")
|
|
assert_equals "workflow_dispatch delete release/2.31 honors inputs" \
|
|
"$actual" \
|
|
$'ACTION=delete\nREF=release/2.31'
|
|
|
|
# ---------------------------------------------------------------
|
|
start_section "release.published event (new in DOCS-327)"
|
|
# ---------------------------------------------------------------
|
|
|
|
actual=$(run_case "release" "v2.35.0" "false" "" "" "")
|
|
assert_equals "stable v2.35.0 resolves to release/2.35" \
|
|
"$actual" \
|
|
$'::notice::Release v2.35.0 resolved to ref release/2.35.\nACTION=index\nREF=release/2.35'
|
|
|
|
actual=$(run_case "release" "v2.35.0-rc.1" "true" "" "" "")
|
|
assert_equals "marked prerelease v2.35.0-rc.1 is skipped, no ACTION/REF" \
|
|
"$actual" \
|
|
'::notice::Skipping prerelease v2.35.0-rc.1; no docs reindex.'
|
|
|
|
actual=$(run_case "release" "v2.35.0-rc.1" "false" "" "" "")
|
|
assert_equals "rc tag without prerelease flag fails regex and is skipped" \
|
|
"$actual" \
|
|
'::notice::Skipping v2.35.0-rc.1: not a plain vX.Y.Z release tag.'
|
|
|
|
actual=$(run_case "release" "v2.35" "false" "" "" "")
|
|
assert_equals "two-segment v2.35 fails regex and is skipped" \
|
|
"$actual" \
|
|
'::notice::Skipping v2.35: not a plain vX.Y.Z release tag.'
|
|
|
|
actual=$(run_case "release" "release-2.35" "false" "" "" "")
|
|
assert_equals "release-2.35 fails regex and is skipped" \
|
|
"$actual" \
|
|
'::notice::Skipping release-2.35: not a plain vX.Y.Z release tag.'
|
|
|
|
# v0.0.0 satisfies the regex by design. Defense in depth lives in the
|
|
# downstream allowlist gate and the workflow's main|release/* case
|
|
# validator; this test pins the regex behavior so a future tightening
|
|
# is intentional.
|
|
actual=$(run_case "release" "v0.0.0" "false" "" "" "")
|
|
assert_equals "v0.0.0 satisfies the regex; allowlist is the gate" \
|
|
"$actual" \
|
|
$'::notice::Release v0.0.0 resolved to ref release/0.0.\nACTION=index\nREF=release/0.0'
|
|
|
|
# Empty tag with prerelease unset reaches the non-semver skip and
|
|
# prints <unknown> for the tag. The :- defaults in the workflow
|
|
# determine the substitution; this test pins both.
|
|
actual=$(EVENT_NAME=release \
|
|
GITHUB_REF_NAME='' \
|
|
INPUT_ACTION='' \
|
|
INPUT_REF='' \
|
|
RELEASE_TAG='' \
|
|
RELEASE_PRERELEASE='' \
|
|
compute_action_ref)
|
|
assert_equals "empty tag with prerelease unset prints <unknown> and skips" \
|
|
"$actual" \
|
|
'::notice::Skipping <unknown>: not a plain vX.Y.Z release tag.'
|
|
|
|
# ---------------------------------------------------------------
|
|
start_section "regex boundary cases"
|
|
# ---------------------------------------------------------------
|
|
|
|
# Multi-digit minor and patch components should resolve, since
|
|
# backports may carry doc updates worth reindexing.
|
|
actual=$(run_case "release" "v2.100.42" "false" "" "" "")
|
|
assert_equals "multi-digit minor and patch resolve correctly" \
|
|
"$actual" \
|
|
$'::notice::Release v2.100.42 resolved to ref release/2.100.\nACTION=index\nREF=release/2.100'
|
|
|
|
# Trailing build metadata is not a plain vX.Y.Z, so it is skipped.
|
|
actual=$(run_case "release" "v2.35.0+build.1" "false" "" "" "")
|
|
assert_equals "semver build metadata is skipped" \
|
|
"$actual" \
|
|
'::notice::Skipping v2.35.0+build.1: not a plain vX.Y.Z release tag.'
|
|
|
|
# Leading whitespace is not a plain vX.Y.Z; the workflow rejects
|
|
# malformed tags instead of trimming them.
|
|
actual=$(run_case "release" " v2.35.0" "false" "" "" "")
|
|
assert_equals "leading whitespace fails the regex" \
|
|
"$actual" \
|
|
$'::notice::Skipping v2.35.0: not a plain vX.Y.Z release tag.'
|
|
|
|
if [ "$failures" -gt 0 ]; then
|
|
echo
|
|
echo "$failures test(s) failed."
|
|
exit 1
|
|
fi
|
|
|
|
echo
|
|
echo "All tests passed."
|