From 7d34acc3190d92e80b7213ae2f5f11fd64f0f23c Mon Sep 17 00:00:00 2001 From: Marie Chatfield Date: Wed, 15 Jul 2026 12:18:58 -0700 Subject: [PATCH 1/2] ci: add missing files to patch snapshot refresh and notify on structural changes The refresh logic only updated files already in the versioned snapshot, so renames and additions in a patch release were silently skipped. This left the snapshot with broken links (updated content pointing to files that didn't exist yet). Now also copies files present in live docs/ but absent from the snapshot. Orphaned files (in snapshot but not in live docs) are still left as-is so existing external links keep resolving. When a patch refresh includes structural changes (additions or orphans), a Slack notification fires so someone can verify the snapshot looks right. Requires SLACK_WEBHOOK_URL in the publish-docs environment; silently skips the notification if not set. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/publish-docs.yaml | 41 +++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml index c19c7b656..4cc8fb4f3 100644 --- a/.github/workflows/publish-docs.yaml +++ b/.github/workflows/publish-docs.yaml @@ -33,10 +33,12 @@ name: Sync Docs Source # # Patch refresh: walks versioned_docs/version-/ and refreshes the # content of each file from the matching path in the freshly-synced docs/. -# Files in the snapshot that have no live counterpart are left as-is. -# Files in live docs/ that aren't already in the snapshot are NOT added — -# adding a doc to a released minor is an intentional change made directly -# in the downstream repo. +# Files in live docs/ that are absent from the snapshot are added — a patch +# may rename or add files, and without them updated cross-links break the +# build. Files in the snapshot with no live counterpart (orphans from a +# rename or deletion) are left as-is so existing external links keep +# resolving. If any structural changes occur (additions or orphans), a +# Slack notification fires so someone can verify the snapshot looks right. # # Auth: a GitHub App installed on Gusto/embedded-sdk-docs with # Contents: write. Credentials live in this repo's `publish-docs` @@ -136,6 +138,7 @@ jobs: SDK_VERSION: ${{ steps.version.outputs.version }} MINOR: ${{ steps.version.outputs.minor }} TRIGGER: ${{ github.event_name }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail @@ -230,6 +233,8 @@ jobs: fi updated=0 orphan=0 + added=0 + # Update content for files already in the snapshot. while IFS= read -r snap_file; do rel="${snap_file#${VERSIONED_DIR}/}" live="docs/${rel}" @@ -237,10 +242,36 @@ jobs: cp "$live" "$snap_file" updated=$((updated + 1)) else + # File exists in snapshot but not in live docs — structural change + # (rename or deletion upstream). Leave the orphan in place so + # existing external links keep resolving; notify below. orphan=$((orphan + 1)) fi done < <(find "$VERSIONED_DIR" -type f) - echo "Refreshed ${updated} file(s) in version-${MINOR}; ${orphan} orphan(s) left as-is." + # Add files present in live docs/ but absent from the snapshot. + # This covers renames and new files added in a patch — without them + # updated content that links to the new paths would break the build. + while IFS= read -r live_file; do + rel="${live_file#docs/}" + snap="${VERSIONED_DIR}/${rel}" + if [ ! -f "$snap" ]; then + mkdir -p "$(dirname "$snap")" + cp "$live_file" "$snap" + added=$((added + 1)) + fi + done < <(find docs -type f) + echo "Refreshed ${updated} file(s) in version-${MINOR}; ${added} added; ${orphan} orphan(s) left as-is." + # Structural changes in a patch (renames, additions, deletions) mean + # the live docs diverged structurally from the snapshot. Notify so + # someone can verify the versioned snapshot still looks right. + if [ "${added}" -gt 0 ] || [ "${orphan}" -gt 0 ]; then + echo "::warning::SDK ${SDK_VERSION} patch had structural doc changes: ${added} file(s) added to snapshot, ${orphan} orphan(s) left in place." + if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then + curl -s -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\":warning: *SDK ${SDK_VERSION} patch sync* had structural doc changes in the version-${MINOR} snapshot: *${added}* file(s) added, *${orphan}* orphan(s) left in place. Verify the looks correct.\"}" \ + "${SLACK_WEBHOOK_URL}" || true + fi + fi fi # ---- Step 5: stage and validate ---- From 405e3cc26b069ae2c347a76ae47028fce09a175a Mon Sep 17 00:00:00 2001 From: Marie Chatfield Date: Wed, 15 Jul 2026 12:29:06 -0700 Subject: [PATCH 2/2] ci: extract snapshot sync logic into callable script Moves the snapshot decision and file operations out of the inline YAML into .github/scripts/sync-snapshot.sh so it can be run locally: .github/scripts/sync-snapshot.sh \ --downstream ../embedded-sdk-docs \ --trigger workflow_run \ --dry-run The workflow now detects the action before calling the script (so the commit message label is based on pre-run state, not post-run state) and delegates all file work to the script. Drops the Slack webhook approach; structural change warnings are printed to stdout (and become GitHub Actions annotations via ::warning:: in CI). Co-Authored-By: Claude Sonnet 4.6 --- .github/scripts/sync-snapshot.sh | 162 ++++++++++++++++++++++++++++ .github/workflows/publish-docs.yaml | 72 ++----------- 2 files changed, 171 insertions(+), 63 deletions(-) create mode 100755 .github/scripts/sync-snapshot.sh diff --git a/.github/scripts/sync-snapshot.sh b/.github/scripts/sync-snapshot.sh new file mode 100755 index 000000000..4403f8268 --- /dev/null +++ b/.github/scripts/sync-snapshot.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Decides whether to create or refresh the versioned docs snapshot in the +# downstream embedded-sdk-docs repo, then performs the file operations. +# +# Extracted from publish-docs.yaml so the logic can be exercised locally +# without triggering a full CI sync. In CI the workflow handles cloning, +# wiping, copying fresh source, staging, and pushing; this script only owns +# the snapshot decision and file operations. +# +# Usage: +# .github/scripts/sync-snapshot.sh --downstream +# [--trigger workflow_run|workflow_dispatch|push] +# [--version ] +# [--dry-run] +# +# --downstream Path to the local embedded-sdk-docs checkout. Required. +# --trigger Simulates the GitHub Actions event name. Defaults to +# workflow_dispatch (equivalent to a manual run: creates a +# snapshot if missing, skips refresh). +# --version Override the SDK version. Defaults to package.json. +# --dry-run Print what would happen without touching any files. +# +# Snapshot action matrix: +# workflow_run, minor missing -> create +# workflow_run, minor present -> refresh +# workflow_dispatch, minor missing -> create +# workflow_dispatch, minor present -> (no-op — manual runs don't refresh) +# push -> (no-op — push path never touches snapshots) +# +# For the create action the script copies docs/ into the downstream's docs/ +# and then runs `npx docusaurus docs:version`. Run `npm ci` in the downstream +# docs-site/ first if node_modules is absent. + +DOWNSTREAM="" +TRIGGER="workflow_dispatch" +VERSION="" +DRY_RUN=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --downstream) DOWNSTREAM="$2"; shift 2 ;; + --trigger) TRIGGER="$2"; shift 2 ;; + --version) VERSION="$2"; shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + *) echo "Unknown argument: $1" >&2; exit 64 ;; + esac +done + +if [[ -z "$DOWNSTREAM" ]]; then + echo "Error: --downstream is required" >&2 + exit 64 +fi +if [[ ! -d "$DOWNSTREAM" ]]; then + echo "Error: downstream path does not exist: $DOWNSTREAM" >&2 + exit 1 +fi + +# Resolve to the repo root of the SDK (the directory containing docs/ and package.json). +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SDK_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +if [[ -z "$VERSION" ]]; then + VERSION=$(node -p "require('${SDK_ROOT}/package.json').version") +fi +MINOR=$(echo "$VERSION" | cut -d. -f1,2) + +echo "SDK version : ${VERSION} (minor ${MINOR})" +echo "Downstream : ${DOWNSTREAM}" +echo "Trigger : ${TRIGGER}" +[[ "$DRY_RUN" -eq 1 ]] && echo "(dry run — no files will be changed)" +echo "" + +# ---- Decide snapshot action ---- +MINOR_IN_VERSIONS=false +VERSIONS_JSON="${DOWNSTREAM}/docs-site/versions.json" +if [[ -f "$VERSIONS_JSON" ]] \ + && jq -e --arg key "${MINOR}" 'index($key) != null' "$VERSIONS_JSON" > /dev/null 2>&1; then + MINOR_IN_VERSIONS=true +fi + +SNAPSHOT_ACTION=none +if [[ "$TRIGGER" == "workflow_run" ]] || [[ "$TRIGGER" == "workflow_dispatch" ]]; then + if [[ "$MINOR_IN_VERSIONS" == "false" ]]; then + SNAPSHOT_ACTION=create + elif [[ "$TRIGGER" == "workflow_run" ]]; then + SNAPSHOT_ACTION=refresh + fi +fi +echo "Snapshot action: ${SNAPSHOT_ACTION}" +echo "" + +if [[ "$SNAPSHOT_ACTION" == "none" ]]; then + echo "Nothing to do." + exit 0 +fi + +VERSIONED_DIR="${DOWNSTREAM}/docs-site/versioned_docs/version-${MINOR}" + +# ---- Create ---- +if [[ "$SNAPSHOT_ACTION" == "create" ]]; then + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "Would copy ${SDK_ROOT}/docs/ -> ${DOWNSTREAM}/docs/" + echo "Would run: npx docusaurus docs:version ${MINOR} (in ${DOWNSTREAM}/docs-site)" + else + cp -R "${SDK_ROOT}/docs/." "${DOWNSTREAM}/docs/" + ( + cd "${DOWNSTREAM}/docs-site" + [[ ! -d node_modules ]] && npm ci + npx docusaurus docs:version "${MINOR}" + ) + echo "Created snapshot version-${MINOR}." + fi + exit 0 +fi + +# ---- Refresh ---- +if [[ ! -d "$VERSIONED_DIR" ]]; then + echo "Error: ${MINOR} is in versions.json but ${VERSIONED_DIR}/ is missing." >&2 + exit 1 +fi + +updated=0 +orphan=0 +added=0 + +# Update files already in the snapshot. +while IFS= read -r snap_file; do + rel="${snap_file#${VERSIONED_DIR}/}" + live="${SDK_ROOT}/docs/${rel}" + if [[ -f "$live" ]]; then + [[ "$DRY_RUN" -eq 0 ]] && cp "$live" "$snap_file" + updated=$((updated + 1)) + else + echo " orphan (no live counterpart): ${rel}" + orphan=$((orphan + 1)) + fi +done < <(find "$VERSIONED_DIR" -type f | sort) + +# Add files present in live docs/ but absent from the snapshot. +while IFS= read -r live_file; do + rel="${live_file#${SDK_ROOT}/docs/}" + snap="${VERSIONED_DIR}/${rel}" + if [[ ! -f "$snap" ]]; then + echo " add (missing from snapshot): ${rel}" + if [[ "$DRY_RUN" -eq 0 ]]; then + mkdir -p "$(dirname "$snap")" + cp "$live_file" "$snap" + fi + added=$((added + 1)) + fi +done < <(find "${SDK_ROOT}/docs" -type f | sort) + +echo "" +echo "Refreshed ${updated} file(s) in version-${MINOR}; ${added} added; ${orphan} orphan(s) left as-is." + +if [[ "${added}" -gt 0 ]] || [[ "${orphan}" -gt 0 ]]; then + echo "" + echo "WARNING: structural changes detected — ${added} file(s) added, ${orphan} orphan(s)." + echo "Verify that version-${MINOR} snapshot looks correct before merging." +fi diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml index 4cc8fb4f3..a21978258 100644 --- a/.github/workflows/publish-docs.yaml +++ b/.github/workflows/publish-docs.yaml @@ -138,7 +138,6 @@ jobs: SDK_VERSION: ${{ steps.version.outputs.version }} MINOR: ${{ steps.version.outputs.minor }} TRIGGER: ${{ github.event_name }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | set -euo pipefail @@ -197,18 +196,13 @@ jobs: done # ---- Step 4: snapshot creation or refresh ---- - # Snapshot ACTION is determined by trigger + downstream state: - # workflow_run, minor missing -> create (new minor) - # workflow_run, minor present -> refresh (patch) - # workflow_dispatch, missing -> create (bootstrap / recover) - # workflow_dispatch, present -> none - # push -> none + # Determine the action before running the script so we have it + # for the commit message label regardless of what the script changes. MINOR_IN_VERSIONS=false if [ -f docs-site/versions.json ] \ && jq -e --arg key "${MINOR}" 'index($key) != null' docs-site/versions.json > /dev/null 2>&1; then MINOR_IN_VERSIONS=true fi - SNAPSHOT_ACTION=none if [ "$TRIGGER" = "workflow_run" ] || [ "$TRIGGER" = "workflow_dispatch" ]; then if [ "$MINOR_IN_VERSIONS" = "false" ]; then @@ -217,62 +211,14 @@ jobs: SNAPSHOT_ACTION=refresh fi fi - echo "Snapshot action for SDK ${SDK_VERSION} (minor ${MINOR}, trigger ${TRIGGER}): ${SNAPSHOT_ACTION}" - if [ "$SNAPSHOT_ACTION" = "create" ]; then - # docusaurus docs:version copies docs/ -> versioned_docs/version-/, - # captures the sidebar, prepends to versions.json. - # The conditional config picks up versions.json[0] as lastVersion automatically. - (cd docs-site && npm ci && npx docusaurus docs:version "${MINOR}") - echo "Created snapshot version-${MINOR} in downstream." - elif [ "$SNAPSHOT_ACTION" = "refresh" ]; then - VERSIONED_DIR="docs-site/versioned_docs/version-${MINOR}" - if [ ! -d "$VERSIONED_DIR" ]; then - echo "::error::${MINOR} is in versions.json but ${VERSIONED_DIR}/ is missing in downstream." - exit 1 - fi - updated=0 - orphan=0 - added=0 - # Update content for files already in the snapshot. - while IFS= read -r snap_file; do - rel="${snap_file#${VERSIONED_DIR}/}" - live="docs/${rel}" - if [ -f "$live" ]; then - cp "$live" "$snap_file" - updated=$((updated + 1)) - else - # File exists in snapshot but not in live docs — structural change - # (rename or deletion upstream). Leave the orphan in place so - # existing external links keep resolving; notify below. - orphan=$((orphan + 1)) - fi - done < <(find "$VERSIONED_DIR" -type f) - # Add files present in live docs/ but absent from the snapshot. - # This covers renames and new files added in a patch — without them - # updated content that links to the new paths would break the build. - while IFS= read -r live_file; do - rel="${live_file#docs/}" - snap="${VERSIONED_DIR}/${rel}" - if [ ! -f "$snap" ]; then - mkdir -p "$(dirname "$snap")" - cp "$live_file" "$snap" - added=$((added + 1)) - fi - done < <(find docs -type f) - echo "Refreshed ${updated} file(s) in version-${MINOR}; ${added} added; ${orphan} orphan(s) left as-is." - # Structural changes in a patch (renames, additions, deletions) mean - # the live docs diverged structurally from the snapshot. Notify so - # someone can verify the versioned snapshot still looks right. - if [ "${added}" -gt 0 ] || [ "${orphan}" -gt 0 ]; then - echo "::warning::SDK ${SDK_VERSION} patch had structural doc changes: ${added} file(s) added to snapshot, ${orphan} orphan(s) left in place." - if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then - curl -s -X POST -H 'Content-type: application/json' \ - --data "{\"text\":\":warning: *SDK ${SDK_VERSION} patch sync* had structural doc changes in the version-${MINOR} snapshot: *${added}* file(s) added, *${orphan}* orphan(s) left in place. Verify the looks correct.\"}" \ - "${SLACK_WEBHOOK_URL}" || true - fi - fi - fi + # Delegate to the script so this logic can also be run locally. + # In CI $(pwd) is the cloned downstream working tree (docs-repo/). + # The create path runs npm ci inside the downstream docs-site/. + "${GITHUB_WORKSPACE}/.github/scripts/sync-snapshot.sh" \ + --downstream "$(pwd)" \ + --trigger "${TRIGGER}" \ + --version "${SDK_VERSION}" # ---- Step 5: stage and validate ---- # Stage only the paths we manage. NOT `git add -A` — that could