Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions automation/source-repo-templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ it ships its own rustdoc pipeline today.
| `resq-software/viz` | C#/web | DefaultDocumentation | _TODO_ |
| `resq-software/crates` | Rust | already has docs | n/a |

## Syncing changes

Templates here are the canonical version. After editing, push the
update to each source repo with the helper script:

```sh
automation/sync-templates.sh # all 3
automation/sync-templates.sh --dry-run # preview diffs
automation/sync-templates.sh python # one language only
automation/sync-templates.sh --auto-merge # open PRs with --auto
```

The script clones each target repo shallowly, copies the matching
`api-docs.<lang>.yml`, opens a sync PR on `sync/api-docs-template`,
and reports up-to-date when the workflow already matches.

## Adding a new template

1. Drop the workflow YAML in this folder named `api-docs.<lang>.yml`.
Expand Down
4 changes: 2 additions & 2 deletions automation/source-repo-templates/api-docs.dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ jobs:
# Mintlify only routes pages registered in docs.json. The
# _pages.json artifact lists every generated Markdown path;
# rewrite the matching language sub-group under the
# 'Generated API Reference' group so all pages are
# 'Generated Package References' group so all pages are
# discoverable, in-content cross-links resolve, and direct
# URLs work.
working-directory: docs-checkout
Expand Down Expand Up @@ -509,7 +509,7 @@ jobs:

en = next(l for l in docs["navigation"]["languages"] if l["language"] == "en")
sdks_tab = next(t for t in en["tabs"] if t["tab"] == "SDKs")
gen_group = next(g for g in sdks_tab["groups"] if g["group"] == "Generated API Reference")
gen_group = next(g for g in sdks_tab["groups"] if g["group"] == "Generated Package References")
for sub in gen_group["pages"]:
if isinstance(sub, dict) and sub.get("group") == LANG_LABEL:
sub["pages"] = new_pages
Expand Down
4 changes: 2 additions & 2 deletions automation/source-repo-templates/api-docs.python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ jobs:
# Mintlify only routes pages registered in docs.json. The
# _pages.json artifact lists every generated Markdown path;
# rewrite the matching language sub-group under the
# 'Generated API Reference' group so all pages are
# 'Generated Package References' group so all pages are
# discoverable, in-content cross-links resolve, and direct
# URLs work.
working-directory: docs-checkout
Expand Down Expand Up @@ -547,7 +547,7 @@ jobs:

en = next(l for l in docs["navigation"]["languages"] if l["language"] == "en")
sdks_tab = next(t for t in en["tabs"] if t["tab"] == "SDKs")
gen_group = next(g for g in sdks_tab["groups"] if g["group"] == "Generated API Reference")
gen_group = next(g for g in sdks_tab["groups"] if g["group"] == "Generated Package References")
for sub in gen_group["pages"]:
if isinstance(sub, dict) and sub.get("group") == LANG_LABEL:
sub["pages"] = new_pages
Expand Down
4 changes: 2 additions & 2 deletions automation/source-repo-templates/api-docs.typescript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ jobs:
# Mintlify only routes pages registered in docs.json. The
# _pages.json artifact lists every generated Markdown path;
# rewrite the matching language sub-group under the
# 'Generated API Reference' group so all pages are
# 'Generated Package References' group so all pages are
# discoverable, in-content cross-links resolve, and direct
# URLs work.
working-directory: docs-checkout
Expand Down Expand Up @@ -319,7 +319,7 @@ jobs:

en = next(l for l in docs["navigation"]["languages"] if l["language"] == "en")
sdks_tab = next(t for t in en["tabs"] if t["tab"] == "SDKs")
gen_group = next(g for g in sdks_tab["groups"] if g["group"] == "Generated API Reference")
gen_group = next(g for g in sdks_tab["groups"] if g["group"] == "Generated Package References")
for sub in gen_group["pages"]:
if isinstance(sub, dict) and sub.get("group") == LANG_LABEL:
sub["pages"] = new_pages
Expand Down
150 changes: 150 additions & 0 deletions automation/sync-templates.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# Sync canonical SDK auto-doc workflows from this repo's
# `automation/source-repo-templates/` into each SDK source repo's
# `.github/workflows/api-docs.yml`. Opens a sync PR per repo when a
# diff exists; reports up-to-date when not.
#
# Mapping is hard-coded (one entry per language) because there are
# only three SDKs and each has its own repo + default-branch quirk.
#
# Requirements:
# - gh CLI authenticated with repo:write on each target repo
# - git, mktemp
#
# Usage:
# automation/sync-templates.sh # sync all 3
# automation/sync-templates.sh dotnet # sync just dotnet
# automation/sync-templates.sh dotnet python # sync subset
#
# Flags:
# --dry-run show diffs but don't open PRs
# --auto-merge open PRs with --auto so they merge after CI
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMPLATE_DIR="${REPO_ROOT}/automation/source-repo-templates"

# language|template_filename|target_repo|default_branch
TARGETS=(
"typescript|api-docs.typescript.yml|resq-software/npm|master"
"python|api-docs.python.yml|resq-software/pypi|main"
"dotnet|api-docs.dotnet.yml|resq-software/dotnet-sdk|main"
)

DRY_RUN=0
AUTO_MERGE=0
SELECTED_LANGS=()
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--auto-merge) AUTO_MERGE=1 ;;
-*) echo "unknown flag: $arg" >&2; exit 2 ;;
*) SELECTED_LANGS+=("$arg") ;;
esac
done

selected() {
if [ ${#SELECTED_LANGS[@]} -eq 0 ]; then
return 0
fi
for s in "${SELECTED_LANGS[@]}"; do
[ "$s" = "$1" ] && return 0
done
return 1
}

sync_one() {
local lang="$1" template_file="$2" target_repo="$3" default_branch="$4"

if ! selected "$lang"; then
return 0
fi

local template_path="${TEMPLATE_DIR}/${template_file}"
if [ ! -f "$template_path" ]; then
echo "[$lang] missing template: $template_path" >&2
return 1
fi

echo "==> [$lang] checking $target_repo"
local work
work="$(mktemp -d)"
trap 'rm -rf "$work"' RETURN

local clone_dir="${work}/clone"
if ! gh repo clone "$target_repo" "$clone_dir" -- \
--depth=1 --branch="$default_branch" --quiet 2>/dev/null; then
echo "[$lang] clone failed for $target_repo" >&2
Comment on lines +76 to +77
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Hiding stderr from gh repo clone using 2>/dev/null makes it difficult to diagnose failures (e.g., authentication issues or repository permission errors). Removing the redirection will provide better visibility when a sync target fails to clone.

Suggested change
--depth=1 --branch="$default_branch" --quiet 2>/dev/null; then
echo "[$lang] clone failed for $target_repo" >&2
if ! gh repo clone "$target_repo" "$clone_dir" -- \
--depth=1 --branch="$default_branch" --quiet; then

return 1
fi

local target_workflow="${clone_dir}/.github/workflows/api-docs.yml"
mkdir -p "$(dirname "$target_workflow")"
cp "$template_path" "$target_workflow"

if git -C "$clone_dir" diff --quiet -- .github/workflows/api-docs.yml; then
echo "[$lang] up-to-date"
return 0
fi

echo "[$lang] diff:"
git -C "$clone_dir" --no-pager diff --stat -- .github/workflows/api-docs.yml

if [ "$DRY_RUN" -eq 1 ]; then
echo "[$lang] dry-run, skipping PR"
return 0
fi

local branch="sync/api-docs-template"
local commit_msg
commit_msg="$(cat <<EOF
ci(api-docs): sync workflow from resq-software/docs

Sync .github/workflows/api-docs.yml from the canonical template at
automation/source-repo-templates/${template_file} in
resq-software/docs.

Generated by automation/sync-templates.sh.
EOF
)"

(
cd "$clone_dir"
git checkout -b "$branch"
git add .github/workflows/api-docs.yml
git -c user.email=engineer@resq.software \
-c user.name="resq-sw" \
commit -m "$commit_msg"
git push -u origin "$branch" --force-with-lease
)

local pr_url
pr_url="$(gh pr create \
--repo "$target_repo" \
--base "$default_branch" \
--head "$branch" \
--title "ci(api-docs): sync workflow from resq-software/docs" \
--body "Sync from canonical template \`automation/source-repo-templates/${template_file}\`. Auto-generated by \`automation/sync-templates.sh\`." \
2>&1 | tail -1)"
echo "[$lang] $pr_url"
Comment on lines +98 to +129
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

for repo in resq-software/npm resq-software/pypi resq-software/dotnet-sdk; do
  echo "==> $repo"
  gh pr list \
    --repo "$repo" \
    --state open \
    --head sync/api-docs-template \
    --json number,url,headRefName,baseRefName \
    --jq '.[] | "\(.number) \(.url) [\(.headRefName)->\(.baseRefName)]"'
done

Repository: resq-software/docs

Length of output: 136


🌐 Web query:

gh pr create behavior when PR already exists same head base

💡 Result:

When using GitHub CLI (gh) to create a PR, if the head branch and base branch resolve to the exact same ref within the same repository, gh pr create does NOT proceed; it errors out early. Specifically, gh/cli added an isSameRef check that returns true when head and base point to the same ref in the same repo, and createRun then returns: head branch "<head>" is the same as base branch "<base>", cannot create a pull request [1]. Important nuance: the same branch name across different repositories (e.g., fork vs upstream) is not treated as “same ref”; the code avoids the error for cross-repository PRs even if the branch names match [1]. Separately (different scenario), if you try to create a PR when an open PR for the same head branch already exists, gh pr create reports that the PR already exists and aborts (rather than creating/overwriting another PR) [2][3].

Citations:


🏁 Script executed:

# Verify the gh pr list syntax and jq filter from the suggested fix
# Also check if gh pr create indeed fails on duplicate head/base

# Test: Simulate the suggested check command to verify syntax
gh pr list \
  --repo "resq-software/npm" \
  --state open \
  --head "sync/api-docs-template" \
  --base "main" \
  --json url \
  --jq '.[0].url' 2>&1 | head -20

Repository: resq-software/docs

Length of output: 63


Make PR creation idempotent when sync/api-docs-template already has an open PR.

This always calls gh pr create on a fixed head branch. If an open PR already exists, reruns fail and block the workflow. The GitHub CLI aborts with "PR already exists" rather than reusing the existing PR URL.

🛠️ Suggested diff
-  local pr_url
-  pr_url="$(gh pr create \
-    --repo "$target_repo" \
-    --base "$default_branch" \
-    --head "$branch" \
-    --title "ci(api-docs): sync workflow from resq-software/docs" \
-    --body "Sync from canonical template \`automation/source-repo-templates/${template_file}\`. Auto-generated by \`automation/sync-templates.sh\`." \
-    2>&1 | tail -1)"
+  local pr_url
+  pr_url="$(gh pr list \
+    --repo "$target_repo" \
+    --state open \
+    --head "$branch" \
+    --base "$default_branch" \
+    --json url \
+    --jq '.[0].url')"
+  if [ -z "$pr_url" ]; then
+    pr_url="$(gh pr create \
+      --repo "$target_repo" \
+      --base "$default_branch" \
+      --head "$branch" \
+      --title "ci(api-docs): sync workflow from resq-software/docs" \
+      --body "Sync from canonical template \`automation/source-repo-templates/${template_file}\`. Auto-generated by \`automation/sync-templates.sh\`." )"
+  fi
   echo "[$lang] $pr_url"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@automation/sync-templates.sh` around lines 98 - 129, The script currently
always runs gh pr create for the fixed head branch stored in local
branch="sync/api-docs-template", causing failures if a PR already exists; change
the flow in the block that sets pr_url so it first checks for an existing open
PR for that head (use gh pr list or gh pr view with --head "$branch" and --repo
"$target_repo") and if found, capture and echo that existing PR URL instead of
calling gh pr create, otherwise call gh pr create as before; ensure pr_url is
set to the existing PR URL when reusing and preserve the existing commit/push
steps (git checkout -b "$branch", git push -u origin "$branch"
--force-with-lease) so the create-or-reuse logic around gh pr create sets the
same pr_url variable used by the echo.


if [ "$AUTO_MERGE" -eq 1 ]; then
gh pr merge "$pr_url" --repo "$target_repo" --squash --auto --delete-branch || true
echo "[$lang] auto-merge enabled"
fi
Comment on lines +122 to +134
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current PR creation and merging logic has several issues:

  1. Brittle URL capture: Using 2>&1 | tail -1 is unreliable as it mixes stderr and stdout, potentially capturing error messages as URLs if the command fails.
  2. Existing PR handling: gh pr create fails if a PR already exists for the branch. The script should handle this by fetching the existing PR's URL to allow the sync to continue (e.g., for auto-merging).
  3. Incorrect return status: The function returns 0 even if PR creation or merging fails because the last command executed is an echo. This prevents the main loop from correctly reporting failures.

The suggested refactor addresses these by using a safer capture method, adding a fallback for existing PRs, and ensuring non-zero exit codes on failure.

  if ! pr_url=$(gh pr create \
    --repo "$target_repo" \
    --base "$default_branch" \
    --head "$branch" \
    --title "ci(api-docs): sync workflow from resq-software/docs" \
    --body "Sync from canonical template \`automation/source-repo-templates/${template_file}\`. Auto-generated by \`automation/sync-templates.sh\`." 2>/dev/null); then
    # Fallback: if PR already exists, fetch its URL
    pr_url=$(gh pr view "$branch" --repo "$target_repo" --json url -q .url 2>/dev/null || true)
  fi

  if [ -z "$pr_url" ]; then
    echo "[$lang] failed to create or find PR" >&2
    return 1
  fi
  echo "[$lang] $pr_url"

  if [ "$AUTO_MERGE" -eq 1 ]; then
    if ! gh pr merge "$pr_url" --repo "$target_repo" --squash --auto --delete-branch; then
      echo "[$lang] auto-merge failed" >&2
      return 1
    fi
    echo "[$lang] auto-merge enabled"
  fi

}

failed=0
for entry in "${TARGETS[@]}"; do
IFS='|' read -r lang template repo branch <<<"$entry"
if ! sync_one "$lang" "$template" "$repo" "$branch"; then
failed=$((failed + 1))
fi
done

if [ "$failed" -gt 0 ]; then
echo "==> $failed sync target(s) failed" >&2
exit 1
fi

echo "==> done"
2 changes: 1 addition & 1 deletion docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
]
},
{
"group": "Generated API Reference",
"group": "Generated Package References",
"pages": [
{
"group": "TypeScript",
Expand Down
6 changes: 3 additions & 3 deletions scripts/splice-sdk-nav.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Splice each SDK's _pages.json into docs.json's `Generated API Reference`
"""Splice each SDK's _pages.json into docs.json's `Generated Package References`
sub-group for that language.

Builds a hierarchical groups structure from page IDs of the form
Expand Down Expand Up @@ -161,12 +161,12 @@ def main() -> int:
readme_id = f"{prefix}/README"
new_subgroups.append(build_lang_group(label, prefix, pages_path, readme_id))

# Find Generated API Reference under en > SDKs > groups
# Find Generated Package References under en > SDKs > groups
en = next(l for l in docs["navigation"]["languages"] if l["language"] == "en")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rename the ambiguous generator variable to satisfy Ruff E741.

next(l for l in ...) is flagged as ambiguous and hurts readability. Rename l to a descriptive identifier.

🧩 Suggested diff
-    en = next(l for l in docs["navigation"]["languages"] if l["language"] == "en")
+    en = next(lang_entry for lang_entry in docs["navigation"]["languages"] if lang_entry["language"] == "en")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
en = next(l for l in docs["navigation"]["languages"] if l["language"] == "en")
en = next(lang_entry for lang_entry in docs["navigation"]["languages"] if lang_entry["language"] == "en")
🧰 Tools
🪛 Ruff (0.15.12)

[error] 165-165: Ambiguous variable name: l

(E741)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/splice-sdk-nav.py` at line 165, The generator expression that builds
the English language entry uses an ambiguous variable name `l`; update the
expression in the assignment to `en = next(...)` to use a descriptive identifier
(e.g., `lang` or `language_entry`) instead of `l` so it satisfies Ruff E741 and
improves readability—locate the `en = next(l for l in
docs["navigation"]["languages"] if l["language"] == "en")` line and replace the
`l` identifier throughout that generator expression with the chosen descriptive
name.

sdks_tab = next(t for t in en["tabs"] if t["tab"] == "SDKs")
gen_group = next(
g for g in sdks_tab["groups"]
if g["group"] == "Generated API Reference"
if g["group"] == "Generated Package References"
)
gen_group["pages"] = new_subgroups

Expand Down
Loading