Skip to content

sync-docs

sync-docs #182

Workflow file for this run

name: sync-docs
on:
workflow_dispatch:
schedule:
# Runs every 12 hours
- cron: '0 */12 * * *'
env:
DOCS_PATH: 'docs'
# Space-separated list of subfolders the safety guard tracks. Must match what
# the update scripts produce.
DOCS_FOLDERS: 'api agents-and-tools build-with-claude manage-claude managed-agents test-and-evaluate release-notes general code'
MAX_REMOVED_FILES_PERCENTAGE: "${{ vars.MAX_REMOVED_FILES_PERCENTAGE || '25' }}"
jobs:
update-and-publish:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
########################################################################
# GENERATE DOCS
########################################################################
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: pnpm install
- name: Run docs update script
run: bun scripts/update-docs.js
- name: Cleanup README files when docs are unchanged
run: |
for folder_name in ${DOCS_FOLDERS}; do
README="${DOCS_PATH}/${folder_name}/${folder_name}-README.md"
[ -f "$README" ] || continue
REAL_CHANGES=$(git status --porcelain "${DOCS_PATH}/${folder_name}/" \
| grep -v "${folder_name}-README.md" || true)
if [ -z "$REAL_CHANGES" ]; then
echo "No real changes in ${folder_name}, reverting README"
git checkout -- "$README"
else
echo "Real changes detected in ${folder_name}, keeping README"
fi
done
- name: Check for file changes
id: git_status
run: |
echo "Max allowed removed percentage: ${MAX_REMOVED_FILES_PERCENTAGE}%"
echo "Docs path: ${DOCS_PATH}"
DOCS_PATHS=()
for folder_name in ${DOCS_FOLDERS}; do
DOCS_PATHS+=("${DOCS_PATH}/${folder_name}/")
done
CHANGED_FILES="$(git status --porcelain "${DOCS_PATHS[@]}" || true)"
if [ -z "$CHANGED_FILES" ]; then
echo "No relevant changes detected."
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Relevant changes detected:"
echo "$CHANGED_FILES"
echo "has_changes=true" >> $GITHUB_OUTPUT
CRITICAL=false
if git rev-parse --verify --quiet HEAD >/dev/null; then
HAVE_HEAD=true
else
HAVE_HEAD=false
fi
for folder_name in ${DOCS_FOLDERS}; do
FOLDER_PATH="${DOCS_PATH}/${folder_name}"
if [ ! -d "$FOLDER_PATH" ]; then
echo "❌ CRITICAL: Folder '${FOLDER_PATH}' is missing in working tree. Aborting."
CRITICAL=true
continue
fi
if [ "$HAVE_HEAD" = true ]; then
TOTAL_FOLDER_FILES="$(git ls-tree -r --name-only HEAD -- "$FOLDER_PATH" | wc -l | tr -d '[:space:]')"
REMOVED_FOLDER_FILES="$(git diff --name-only --diff-filter=D HEAD -- "$FOLDER_PATH" 2>/dev/null | wc -l | tr -d '[:space:]')"
else
CURRENT_FOLDER_FILES="$(find "$FOLDER_PATH" -type f 2>/dev/null | wc -l | tr -d '[:space:]')"
REMOVED_FOLDER_FILES="$(echo "$CHANGED_FILES" | grep "^ D " | grep "$FOLDER_PATH" | wc -l | tr -d '[:space:]')"
if [ "$CURRENT_FOLDER_FILES" -eq 0 ] && [ "$REMOVED_FOLDER_FILES" -eq 0 ]; then
TOTAL_FOLDER_FILES=0
else
TOTAL_FOLDER_FILES=$(( CURRENT_FOLDER_FILES + REMOVED_FOLDER_FILES ))
fi
fi
TOTAL_FOLDER_FILES="${TOTAL_FOLDER_FILES:-0}"
REMOVED_FOLDER_FILES="${REMOVED_FOLDER_FILES:-0}"
if [ "$TOTAL_FOLDER_FILES" -eq 0 ]; then
echo "❌ CRITICAL: Folder '$FOLDER_PATH' has 0 files in total (before deletion). Aborting."
CRITICAL=true
continue
fi
FOLDER_PERCENTAGE=$(( REMOVED_FOLDER_FILES * 100 / TOTAL_FOLDER_FILES ))
echo "Folder check: $folder_name → removed ${REMOVED_FOLDER_FILES}/${TOTAL_FOLDER_FILES} (${FOLDER_PERCENTAGE}%)"
if [ "$FOLDER_PERCENTAGE" -ge "$MAX_REMOVED_FILES_PERCENTAGE" ]; then
echo "❌ CRITICAL: Folder '${folder_name}' exceeds safe deletion threshold (${FOLDER_PERCENTAGE}% ≥ ${MAX_REMOVED_FILES_PERCENTAGE}%)."
CRITICAL=true
fi
done
if [ "$CRITICAL" = true ]; then
echo "❌ CRITICAL: deletion threshold exceeded or missing folders detected. Please check logs above. Aborting."
exit 1
fi
echo "All folder checks passed."
exit 0
########################################################################
# VERSION + RELEASE
########################################################################
- name: Configure Git user
if: steps.git_status.outputs.has_changes == 'true'
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "actions-bot@github.com"
- name: Bump package version
if: steps.git_status.outputs.has_changes == 'true'
id: version
run: |
pnpm version patch --no-git-tag-version --commit-hooks false
NEW_VERSION=$(node -p "require('./package.json').version")
echo "version_number=${NEW_VERSION}" >> $GITHUB_OUTPUT
- name: Generate Changelog
id: changelog_generator
if: steps.git_status.outputs.has_changes == 'true'
run: |
NEW_VERSION="${{ steps.version.outputs.version_number }}"
HEADER="## 🤖 v${NEW_VERSION} - $(date +'%d/%m/%Y')"
echo -e "$HEADER\n\nFile Changes:\n" > new_entry.tmp
DOCS_PATHS=()
for folder_name in ${DOCS_FOLDERS}; do
DOCS_PATHS+=("${DOCS_PATH}/${folder_name}/")
done
git status --porcelain "${DOCS_PATHS[@]}" \
| while IFS= read -r line; do
case "$line" in
" M "*) echo "- Modified: \`${line:3}\`" >> new_entry.tmp ;;
" D "*) echo "- Deleted: \`${line:3}\`" >> new_entry.tmp ;;
"?? "*) echo "- Added: \`${line:3}\`" >> new_entry.tmp ;;
esac
done
echo "body<<EOF" >> $GITHUB_OUTPUT
cat new_entry.tmp >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
tail -n +2 CHANGELOG.md 2>/dev/null > old.tmp
{ echo "# Changelog"; echo ""; cat new_entry.tmp; echo ""; cat old.tmp; } > CHANGELOG.md
rm new_entry.tmp old.tmp
- name: Commit, Tag, and Push
if: steps.git_status.outputs.has_changes == 'true'
run: |
DOCS_PATHS=()
for folder_name in ${DOCS_FOLDERS}; do
DOCS_PATHS+=("${DOCS_PATH}/${folder_name}/")
done
git add "${DOCS_PATHS[@]}" \
"${DOCS_PATH}/INDEX.md" \
CHANGELOG.md package.json pnpm-lock.yaml
git commit -m "docs: release v${{ steps.version.outputs.version_number }}"
git tag "${{ steps.version.outputs.version_number }}"
git push
git push --tags
- name: Create GitHub Release
if: steps.git_status.outputs.has_changes == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: "${{ steps.version.outputs.version_number }}"
name: "${{ steps.version.outputs.version_number }}"
body: "${{ steps.changelog_generator.outputs.body }}"