From 8826d41f8b8c64dbca761e46dfdc26461f921357 Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Tue, 23 Jun 2026 16:33:06 -0400 Subject: [PATCH 1/8] adding automation to automate documentation drafting --- .github/workflows/issue_autodoc.yml | 127 ++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .github/workflows/issue_autodoc.yml diff --git a/.github/workflows/issue_autodoc.yml b/.github/workflows/issue_autodoc.yml new file mode 100644 index 00000000000..af0e4f59561 --- /dev/null +++ b/.github/workflows/issue_autodoc.yml @@ -0,0 +1,127 @@ +name: Autodoc — Epic Documentation Audit + +on: + issues: + types: [closed] + +jobs: + preflight: + name: Determine whether to run + # Run if the issue carries any of the three trigger labels. + if: | + contains(github.event.issue.labels.*.name, 'Epic') || + contains(github.event.issue.labels.*.name, 'Doc : Needs Doc') || + contains(github.event.issue.labels.*.name, 'Changelog: Needs Doc') + runs-on: ubuntu-latest + permissions: + issues: read + outputs: + should_run: ${{ steps.check.outputs.should_run }} + + steps: + - name: Check run conditions + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + # Epic label present means unconditional run — no further checks needed. + HAS_EPIC: ${{ contains(github.event.issue.labels.*.name, 'Epic') }} + run: | + if [ "$HAS_EPIC" = "true" ]; then + echo "Issue #$ISSUE_NUMBER has Epic label — proceeding unconditionally." + echo "should_run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Non-Epic issue: check whether it is a sub-task of an Epic. + # If so, skip — the parent Epic closure will handle documentation. + PARENT_LABELS=$(gh api graphql \ + -f query=' + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $number) { + parent { + labels(first: 20) { nodes { name } } + } + } + } + } + ' \ + -f owner="${REPO%/*}" \ + -f repo="${REPO#*/}" \ + -F number="$ISSUE_NUMBER" \ + --jq '.data.repository.issue.parent.labels.nodes[].name // empty') + + if echo "$PARENT_LABELS" | grep -qx "Epic"; then + echo "Issue #$ISSUE_NUMBER is a sub-task of an Epic — skipping; Epic closure will handle docs." + echo "should_run=false" >> "$GITHUB_OUTPUT" + else + echo "Issue #$ISSUE_NUMBER has a doc label and is not an Epic sub-task — proceeding." + echo "should_run=true" >> "$GITHUB_OUTPUT" + fi + + autodoc: + name: Run documentation audit + needs: preflight + if: needs.preflight.outputs.should_run == 'true' + runs-on: ubuntu-latest + permissions: + issues: write # post/edit the report comment + contents: read + + env: + EPIC_NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + AUTODOC_DOTCMS_API_TOKEN_AISEARCH: ${{ secrets.AUTODOC_DOTCMS_API_TOKEN_AISEARCH }} + AUTODOC_DOTCMS_API_TOKEN_DRAFTING: ${{ secrets.AUTODOC_DOTCMS_API_TOKEN_DRAFTING }} + AUTODOC_DOTCMS_BASE_URL: ${{ secrets.AUTODOC_DOTCMS_BASE_URL }} + AUTODOC_DOTCMS_SITE_FOLDER: ${{ secrets.AUTODOC_DOTCMS_SITE_FOLDER }} + + steps: + - name: Checkout triggering repo (for gh context) + uses: actions/checkout@v4 + + - name: Checkout dotcms-aios (includes autodoc scripts) + uses: actions/checkout@v4 + with: + repository: dotCMS/dotcms-aios + token: ${{ secrets.CI_MACHINE_TOKEN }} + path: dotcms-aios + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + working-directory: dotcms-aios/autodoc + + - name: Install autodoc dependencies + working-directory: dotcms-aios/autodoc + run: uv sync + + - name: Install Claude Code CLI + run: npm install -g @anthropic-ai/claude-code + + - name: Run eval → Claude + working-directory: dotcms-aios/autodoc + run: | + uv run python scripts/run_eval.py \ + --epic "$EPIC_NUMBER" \ + --repo "$REPO" \ + --prompt burlap \ + --aios-dir ../ \ + > /tmp/eval_context.md + + claude --print --allowedTools Bash,Write < /tmp/eval_context.md + + - name: Run finalize (apply + post comment + commit) + working-directory: dotcms-aios/autodoc + run: | + git config user.name "autodoc[bot]" + git config user.email "autodoc-bot@users.noreply.github.com" + + uv run python scripts/finalize.py \ + --epic "$EPIC_NUMBER" \ + --prompt burlap \ + --repo "$REPO" From 3fa64014f7b6ff9a69d3b893504dcaffbe94aea3 Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Tue, 23 Jun 2026 17:20:23 -0400 Subject: [PATCH 2/8] feat(autodoc): rewrite workflow to be self-contained (no uv) Replace uv/Python-script-based eval and finalize steps with: - Python3 stdlib context assembly using gh CLI and grep - Shell finalize using gh issue comment, curl for dotCMS API, and git commit/push - prompts/burlap.txt read directly via cat (no YAML parsing) - Fix jq null parent bug in preflight guard Eliminates dependency on dotcms-aios PR merge order. --- .github/workflows/issue_autodoc.yml | 220 +++++++++++++++++++++++----- 1 file changed, 187 insertions(+), 33 deletions(-) diff --git a/.github/workflows/issue_autodoc.yml b/.github/workflows/issue_autodoc.yml index af0e4f59561..a8a5c9db901 100644 --- a/.github/workflows/issue_autodoc.yml +++ b/.github/workflows/issue_autodoc.yml @@ -7,7 +7,6 @@ on: jobs: preflight: name: Determine whether to run - # Run if the issue carries any of the three trigger labels. if: | contains(github.event.issue.labels.*.name, 'Epic') || contains(github.event.issue.labels.*.name, 'Doc : Needs Doc') || @@ -25,7 +24,6 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE_NUMBER: ${{ github.event.issue.number }} REPO: ${{ github.repository }} - # Epic label present means unconditional run — no further checks needed. HAS_EPIC: ${{ contains(github.event.issue.labels.*.name, 'Epic') }} run: | if [ "$HAS_EPIC" = "true" ]; then @@ -34,8 +32,6 @@ jobs: exit 0 fi - # Non-Epic issue: check whether it is a sub-task of an Epic. - # If so, skip — the parent Epic closure will handle documentation. PARENT_LABELS=$(gh api graphql \ -f query=' query($owner: String!, $repo: String!, $number: Int!) { @@ -51,7 +47,7 @@ jobs: -f owner="${REPO%/*}" \ -f repo="${REPO#*/}" \ -F number="$ISSUE_NUMBER" \ - --jq '.data.repository.issue.parent.labels.nodes[].name // empty') + --jq '(.data.repository.issue.parent.labels.nodes // []) | .[].name') if echo "$PARENT_LABELS" | grep -qx "Epic"; then echo "Issue #$ISSUE_NUMBER is a sub-task of an Epic — skipping; Epic closure will handle docs." @@ -67,7 +63,7 @@ jobs: if: needs.preflight.outputs.should_run == 'true' runs-on: ubuntu-latest permissions: - issues: write # post/edit the report comment + issues: write contents: read env: @@ -81,47 +77,205 @@ jobs: AUTODOC_DOTCMS_SITE_FOLDER: ${{ secrets.AUTODOC_DOTCMS_SITE_FOLDER }} steps: - - name: Checkout triggering repo (for gh context) + - name: Checkout triggering repo uses: actions/checkout@v4 - - name: Checkout dotcms-aios (includes autodoc scripts) + - name: Checkout dotcms-aios uses: actions/checkout@v4 with: repository: dotCMS/dotcms-aios token: ${{ secrets.CI_MACHINE_TOKEN }} path: dotcms-aios - - name: Set up uv - uses: astral-sh/setup-uv@v5 - with: - working-directory: dotcms-aios/autodoc - - - name: Install autodoc dependencies - working-directory: dotcms-aios/autodoc - run: uv sync - - name: Install Claude Code CLI run: npm install -g @anthropic-ai/claude-code - - name: Run eval → Claude - working-directory: dotcms-aios/autodoc + - name: Build eval context run: | - uv run python scripts/run_eval.py \ - --epic "$EPIC_NUMBER" \ - --repo "$REPO" \ - --prompt burlap \ - --aios-dir ../ \ - > /tmp/eval_context.md + cat > /tmp/ctx.py << 'PYEOF' + import json, os, subprocess, sys + + epic_num = os.environ['EPIC_NUMBER'] + repo = os.environ['REPO'] + MAX_COMMENT_CHARS = 4000 + + result = subprocess.run( + ['gh', 'issue', 'view', epic_num, '--repo', repo, + '--json', 'number,title,body,labels,state,url,' + 'closedByPullRequestsReferences,comments'], + capture_output=True, text=True, check=True) + epic = json.loads(result.stdout) + + labels = ', '.join(l['name'] for l in epic.get('labels', [])) + pr_nums = [r['number'] for r in epic.get('closedByPullRequestsReferences', [])] + + def render_comments(comments): + parts, skipped = [], 0 + for c in (comments or []): + body = (c.get('body') or '').strip() + if not body: + continue + if len(body) > MAX_COMMENT_CHARS: + skipped += 1 + continue + login = (c.get('author') or {}).get('login', '?') + parts.append(f'**{login}:** {body}') + out = '\n\n'.join(parts) + if skipped: + out += (f'\n\n*({skipped} comment{"s" if skipped > 1 else ""}' + f' omitted — exceeded {MAX_COMMENT_CHARS}-character limit)*') + return out - claude --print --allowedTools Bash,Write < /tmp/eval_context.md + lines = [ + '# Evaluation Context', '', + f'## Epic: #{epic["number"]} — {repo}', + f'**Title:** {epic["title"]}', + f'**URL:** {epic.get("url", "")}', + f'**State:** {epic.get("state", "?")}', + f'**Labels:** {labels or "(none)"}', + f'**Related PRs:** {", ".join(f"#{n}" for n in pr_nums) or "(none)"}', + '', '### Epic Body', '', + (epic.get('body') or '').strip(), + ] + ct = render_comments(epic.get('comments')) + if ct: + lines += ['', '### Comments', '', ct] - - name: Run finalize (apply + post comment + commit) - working-directory: dotcms-aios/autodoc + for pr_num in pr_nums: + r = subprocess.run( + ['gh', 'pr', 'view', str(pr_num), '--repo', repo, + '--json', 'number,title,body,state,mergedAt,author,comments'], + capture_output=True, text=True) + if r.returncode != 0: + continue + pr = json.loads(r.stdout) + author = (pr.get('author') or {}).get('login', '?') + lines += [ + '', '---', '', + f'## Related PR: #{pr["number"]} — {pr["title"]}', + f'**State:** {pr.get("state","?")} | ' + f'**Author:** {author} | ' + f'**Merged:** {pr.get("mergedAt") or "N/A"}', + '', + (pr.get('body') or '').strip(), + ] + pct = render_comments(pr.get('comments')) + if pct: + lines += ['', '### Comments', '', pct] + + g = subprocess.run( + ['grep', '-rl', f'/issues/{epic_num}', 'dotcms-aios/work/epics/'], + capture_output=True, text=True) + vault_file = g.stdout.strip().split('\n')[0] if g.stdout.strip() else '' + if vault_file: + lines += ['', '---', '', '## Vault Context', '', + open(vault_file).read().strip()] + else: + print(f'warning: no vault file for Epic #{epic_num}', file=sys.stderr) + + prompt = open('dotcms-aios/autodoc/prompts/burlap.txt').read().strip() + report_path = f'dotcms-aios/autodoc/reports/Epic-{epic_num}_burlap.md' + lines += [ + '', '---', '', '## Prompt: burlap', '', prompt, + '', '---', '', + f'**Report path:** `{report_path}`', + '', + 'Use the Write tool to write the report to exactly that path.', + ] + + with open('/tmp/eval_context.md', 'w') as f: + f.write('\n'.join(lines)) + PYEOF + python3 /tmp/ctx.py + + - name: Run Claude + run: claude --print --allowedTools Bash,Write < /tmp/eval_context.md + + - name: Post comment, apply to dotCMS, commit report run: | + REPORT="dotcms-aios/autodoc/reports/Epic-${EPIC_NUMBER}_burlap.md" + + if [ ! -f "$REPORT" ]; then + echo "No report at $REPORT — skipping finalize." + exit 0 + fi + + # Post report as issue comment (edit existing autodoc comment if one exists) + EXISTING=$(gh api "repos/$REPO/issues/$EPIC_NUMBER/comments" \ + --jq '.[] | select(.body | contains("")) | .id' \ + | head -1) + if [ -n "$EXISTING" ]; then + gh api --method PATCH "repos/$REPO/issues/comments/$EXISTING" \ + -f "body=@$REPORT" + else + gh issue comment "$EPIC_NUMBER" --repo "$REPO" --body-file "$REPORT" + fi + + # Parse action and urlTitle from the machine-readable meta block + ACTION=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^action:/{print $2; exit}' "$REPORT") + URL_TITLE=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^urlTitle:/{print $2; exit}' "$REPORT") + + if [ "$ACTION" = "update" ] && [ -n "$URL_TITLE" ]; then + IDENTIFIER=$(curl -sk -X POST "$AUTODOC_DOTCMS_BASE_URL/api/es/search" \ + -H "Authorization: Bearer $AUTODOC_DOTCMS_API_TOKEN_DRAFTING" \ + -H "Content-Type: application/json" \ + -d "{\"query\":{\"query_string\":{\"query\":\"+contentType:DotcmsDocumentation +DotcmsDocumentation.urlTitle:\\\"$URL_TITLE\\\"\"}},\"size\":1}" \ + | jq -r '(.esresponse[0].hits.hits[0]._source.identifier) // empty') + + if [ -n "$IDENTIFIER" ]; then + export REPORT IDENTIFIER + python3 << 'PYEOF' + import json, os + report = open(os.environ['REPORT']).read() + marker = '' + draft = report.split(marker, 1)[1].lstrip('\n') if marker in report else '' + payload = {'contentlet': { + 'identifier': os.environ['IDENTIFIER'], + 'languageId': 1, 'documentation': draft, + 'disabledWYSIWYG': ['documentation']}} + json.dump(payload, open('/tmp/payload.json', 'w')) + PYEOF + curl -sk -X PUT \ + "$AUTODOC_DOTCMS_BASE_URL/api/v1/workflow/actions/default/fire/EDIT?identifier=$IDENTIFIER" \ + -H "Authorization: Bearer $AUTODOC_DOTCMS_API_TOKEN_DRAFTING" \ + -H "Content-Type: application/json" \ + --data @/tmp/payload.json + fi + + elif [ "$ACTION" = "create" ]; then + TITLE=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^title:/{sub(/^title:[[:space:]]*/,"");print;exit}' "$REPORT") + TAGS=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^tags:/{sub(/^tags:[[:space:]]*/,"");print;exit}' "$REPORT") + SEO=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^seoDescription:/{sub(/^seoDescription:[[:space:]]*/,"");print;exit}' "$REPORT") + export REPORT URL_TITLE TITLE TAGS SEO + python3 << 'PYEOF' + import json, os + report = open(os.environ['REPORT']).read() + marker = '' + draft = report.split(marker, 1)[1].lstrip('\n') if marker in report else '' + sf = os.environ.get('AUTODOC_DOTCMS_SITE_FOLDER', '').strip() + payload = {'contentlet': { + 'contentType': 'DotcmsDocumentation', + 'urlTitle': os.environ['URL_TITLE'], + 'title': os.environ['TITLE'], + 'tag': os.environ['TAGS'], + 'seoDescription': os.environ['SEO'], + 'languageId': 1, 'documentation': draft, + 'disabledWYSIWYG': ['documentation']}} + if sf: + payload['contentlet']['navFolder'] = sf + json.dump(payload, open('/tmp/payload.json', 'w')) + PYEOF + curl -sk -X PUT \ + "$AUTODOC_DOTCMS_BASE_URL/api/v1/workflow/actions/default/fire/NEW" \ + -H "Authorization: Bearer $AUTODOC_DOTCMS_API_TOKEN_DRAFTING" \ + -H "Content-Type: application/json" \ + --data @/tmp/payload.json + fi + + # Commit the report back to dotcms-aios + cd dotcms-aios git config user.name "autodoc[bot]" git config user.email "autodoc-bot@users.noreply.github.com" - - uv run python scripts/finalize.py \ - --epic "$EPIC_NUMBER" \ - --prompt burlap \ - --repo "$REPO" + git add "autodoc/reports/Epic-${EPIC_NUMBER}_burlap.md" + git commit -m "eval: Epic-${EPIC_NUMBER} (burlap)" || true + git push From d8ec756d6cb8ad9beabbe39e9bcacd451002db27 Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Wed, 24 Jun 2026 08:26:12 -0400 Subject: [PATCH 3/8] fix(autodoc): address code review findings in finalize step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix -f → -F on gh api PATCH so the file is read, not sent literally - Prepend marker in finalize before posting so idempotent edit lookup works (marker was not in the report itself) - Add --paginate to comment lookup to search beyond the first page - Use jq first // empty instead of | head -1 to avoid SIGPIPE - Fix urlTitle awk to use sub() consistent with title/tags/seoDescription - Remove -k (TLS bypass); add --fail-with-body to dotCMS curl calls - Add comment clarifying AUTODOC_DOTCMS_API_TOKEN_AISEARCH is used by the Claude subprocess via burlap.txt, not directly by a workflow step --- .github/workflows/issue_autodoc.yml | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/issue_autodoc.yml b/.github/workflows/issue_autodoc.yml index a8a5c9db901..3737168454b 100644 --- a/.github/workflows/issue_autodoc.yml +++ b/.github/workflows/issue_autodoc.yml @@ -71,6 +71,7 @@ jobs: REPO: ${{ github.repository }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # Used by the Claude subprocess when it executes the curl in burlap.txt — not called directly here. AUTODOC_DOTCMS_API_TOKEN_AISEARCH: ${{ secrets.AUTODOC_DOTCMS_API_TOKEN_AISEARCH }} AUTODOC_DOTCMS_API_TOKEN_DRAFTING: ${{ secrets.AUTODOC_DOTCMS_API_TOKEN_DRAFTING }} AUTODOC_DOTCMS_BASE_URL: ${{ secrets.AUTODOC_DOTCMS_BASE_URL }} @@ -200,23 +201,27 @@ jobs: exit 0 fi - # Post report as issue comment (edit existing autodoc comment if one exists) - EXISTING=$(gh api "repos/$REPO/issues/$EPIC_NUMBER/comments" \ - --jq '.[] | select(.body | contains("")) | .id' \ - | head -1) + # Prepend idempotency marker so subsequent runs can find and edit this comment. + { echo ''; cat "$REPORT"; } > /tmp/comment_body.md + + # Edit the existing autodoc comment if one exists, otherwise create a new one. + # --paginate ensures we search all comments, not just the first page. + EXISTING=$(gh api "repos/$REPO/issues/$EPIC_NUMBER/comments" --paginate \ + --jq '[.[] | select(.body | contains("")) | .id] | first // empty') if [ -n "$EXISTING" ]; then gh api --method PATCH "repos/$REPO/issues/comments/$EXISTING" \ - -f "body=@$REPORT" + -F "body=@/tmp/comment_body.md" else - gh issue comment "$EPIC_NUMBER" --repo "$REPO" --body-file "$REPORT" + gh issue comment "$EPIC_NUMBER" --repo "$REPO" --body-file /tmp/comment_body.md fi - # Parse action and urlTitle from the machine-readable meta block + # Parse action and urlTitle from the machine-readable meta block. + # All fields use sub() to preserve the full value after the key prefix. ACTION=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^action:/{print $2; exit}' "$REPORT") - URL_TITLE=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^urlTitle:/{print $2; exit}' "$REPORT") + URL_TITLE=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^urlTitle:/{sub(/^urlTitle:[[:space:]]*/,"");print;exit}' "$REPORT") if [ "$ACTION" = "update" ] && [ -n "$URL_TITLE" ]; then - IDENTIFIER=$(curl -sk -X POST "$AUTODOC_DOTCMS_BASE_URL/api/es/search" \ + IDENTIFIER=$(curl -s --fail-with-body -X POST "$AUTODOC_DOTCMS_BASE_URL/api/es/search" \ -H "Authorization: Bearer $AUTODOC_DOTCMS_API_TOKEN_DRAFTING" \ -H "Content-Type: application/json" \ -d "{\"query\":{\"query_string\":{\"query\":\"+contentType:DotcmsDocumentation +DotcmsDocumentation.urlTitle:\\\"$URL_TITLE\\\"\"}},\"size\":1}" \ @@ -235,7 +240,7 @@ jobs: 'disabledWYSIWYG': ['documentation']}} json.dump(payload, open('/tmp/payload.json', 'w')) PYEOF - curl -sk -X PUT \ + curl -s --fail-with-body -X PUT \ "$AUTODOC_DOTCMS_BASE_URL/api/v1/workflow/actions/default/fire/EDIT?identifier=$IDENTIFIER" \ -H "Authorization: Bearer $AUTODOC_DOTCMS_API_TOKEN_DRAFTING" \ -H "Content-Type: application/json" \ @@ -265,7 +270,7 @@ jobs: payload['contentlet']['navFolder'] = sf json.dump(payload, open('/tmp/payload.json', 'w')) PYEOF - curl -sk -X PUT \ + curl -s --fail-with-body -X PUT \ "$AUTODOC_DOTCMS_BASE_URL/api/v1/workflow/actions/default/fire/NEW" \ -H "Authorization: Bearer $AUTODOC_DOTCMS_API_TOKEN_DRAFTING" \ -H "Content-Type: application/json" \ From 4321a9d3a75ce12d96cc2ce05d8f37557f10ccfd Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Wed, 24 Jun 2026 08:33:05 -0400 Subject: [PATCH 4/8] fix(autodoc): address Bedrock review findings - Make git push conditional on commit succeeding (|| true was causing unconditional push even when there was nothing new to commit) - Add git push origin HEAD for explicit branch targeting - Validate urlTitle is a URL slug before interpolating into JSON query (non-slug chars would break the shell-built -d payload) - Validate IDENTIFIER matches UUID format before placing in URL - Add comment on unpinned npm install explaining the tradeoff --- .github/workflows/issue_autodoc.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/issue_autodoc.yml b/.github/workflows/issue_autodoc.yml index 3737168454b..5128ed3fb9c 100644 --- a/.github/workflows/issue_autodoc.yml +++ b/.github/workflows/issue_autodoc.yml @@ -89,6 +89,8 @@ jobs: path: dotcms-aios - name: Install Claude Code CLI + # Version is unpinned intentionally — claude-code updates frequently and + # the package is published by Anthropic. Pin if supply-chain policy requires it. run: npm install -g @anthropic-ai/claude-code - name: Build eval context @@ -220,6 +222,13 @@ jobs: ACTION=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^action:/{print $2; exit}' "$REPORT") URL_TITLE=$(awk '/BEGIN_DOC_META/{p=1;next} /END_DOC_META/{p=0} p && /^urlTitle:/{sub(/^urlTitle:[[:space:]]*/,"");print;exit}' "$REPORT") + # Validate urlTitle is a URL slug before interpolating into the JSON query. + # Non-slug chars (quotes, backslashes) would break the shell-built JSON payload. + if [ -n "$URL_TITLE" ] && ! echo "$URL_TITLE" | grep -qE '^[a-z0-9][a-z0-9-]*[a-z0-9]$'; then + echo "urlTitle '${URL_TITLE}' is not a valid slug — skipping dotCMS apply." + ACTION=none + fi + if [ "$ACTION" = "update" ] && [ -n "$URL_TITLE" ]; then IDENTIFIER=$(curl -s --fail-with-body -X POST "$AUTODOC_DOTCMS_BASE_URL/api/es/search" \ -H "Authorization: Bearer $AUTODOC_DOTCMS_API_TOKEN_DRAFTING" \ @@ -227,7 +236,8 @@ jobs: -d "{\"query\":{\"query_string\":{\"query\":\"+contentType:DotcmsDocumentation +DotcmsDocumentation.urlTitle:\\\"$URL_TITLE\\\"\"}},\"size\":1}" \ | jq -r '(.esresponse[0].hits.hits[0]._source.identifier) // empty') - if [ -n "$IDENTIFIER" ]; then + # Verify the identifier is UUID-shaped before placing it in a URL. + if echo "$IDENTIFIER" | grep -qE '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$' && [ -n "$IDENTIFIER" ]; then export REPORT IDENTIFIER python3 << 'PYEOF' import json, os @@ -277,10 +287,12 @@ jobs: --data @/tmp/payload.json fi - # Commit the report back to dotcms-aios + # Commit the report back to dotcms-aios. + # Push only runs if commit succeeds (i.e. there were actual changes to commit). cd dotcms-aios git config user.name "autodoc[bot]" git config user.email "autodoc-bot@users.noreply.github.com" git add "autodoc/reports/Epic-${EPIC_NUMBER}_burlap.md" - git commit -m "eval: Epic-${EPIC_NUMBER} (burlap)" || true - git push + if git commit -m "eval: Epic-${EPIC_NUMBER} (burlap)"; then + git push origin HEAD + fi From 3f1955923efdfa746840ca048881cc858ce65a8f Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Wed, 24 Jun 2026 12:31:59 -0400 Subject: [PATCH 5/8] fix(autodoc): use AUTODOC_AIOS_CI for dotcms-aios checkout --- .github/workflows/issue_autodoc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue_autodoc.yml b/.github/workflows/issue_autodoc.yml index 5128ed3fb9c..42fbcf70fc9 100644 --- a/.github/workflows/issue_autodoc.yml +++ b/.github/workflows/issue_autodoc.yml @@ -85,7 +85,7 @@ jobs: uses: actions/checkout@v4 with: repository: dotCMS/dotcms-aios - token: ${{ secrets.CI_MACHINE_TOKEN }} + token: ${{ secrets.AUTODOC_AIOS_CI }} path: dotcms-aios - name: Install Claude Code CLI From 2fdfaa00bb8f71eb70bb6230d9c30c9f6d13fae5 Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Wed, 24 Jun 2026 12:37:24 -0400 Subject: [PATCH 6/8] chore(autodoc): drop git commit/push of report to dotcms-aios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Report lives as an issue comment — the repo copy is redundant. Removing the push also drops the write-scope requirement on AUTODOC_AIOS_CI. --- .github/workflows/issue_autodoc.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/issue_autodoc.yml b/.github/workflows/issue_autodoc.yml index 42fbcf70fc9..89777c91956 100644 --- a/.github/workflows/issue_autodoc.yml +++ b/.github/workflows/issue_autodoc.yml @@ -287,12 +287,3 @@ jobs: --data @/tmp/payload.json fi - # Commit the report back to dotcms-aios. - # Push only runs if commit succeeds (i.e. there were actual changes to commit). - cd dotcms-aios - git config user.name "autodoc[bot]" - git config user.email "autodoc-bot@users.noreply.github.com" - git add "autodoc/reports/Epic-${EPIC_NUMBER}_burlap.md" - if git commit -m "eval: Epic-${EPIC_NUMBER} (burlap)"; then - git push origin HEAD - fi From 5d652c1ec1f035c7d9fc9f4bd62bbccc8937e43b Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Wed, 24 Jun 2026 12:53:44 -0400 Subject: [PATCH 7/8] refactor(autodoc): inline prompt; remove dotcms-aios autodoc/ dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Embed burlap.txt content directly in the workflow as a heredoc step. Update report path to /tmp — report is ephemeral, lives as issue comment. dotcms-aios checkout now solely serves vault epic context lookup. --- .github/workflows/issue_autodoc.yml | 101 +++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue_autodoc.yml b/.github/workflows/issue_autodoc.yml index 89777c91956..506bcebf990 100644 --- a/.github/workflows/issue_autodoc.yml +++ b/.github/workflows/issue_autodoc.yml @@ -93,6 +93,101 @@ jobs: # the package is published by Anthropic. Pin if supply-chain policy requires it. run: npm install -g @anthropic-ai/claude-code + - name: Write prompt + run: | + cat > /tmp/burlap.txt << 'PROMPTEOF' + The context block above contains an Epic with its related PRs already assembled. + Your task is to assess whether this Epic's delivery requires new or updated + documentation in the dotCMS docs. + + If a `## Vault Context` section is present in this block, use it to inform the + grouping and audience sections of the report — target personas, product pillar, + GTM status, and strategic framing. If no Vault Context is present, infer from + the Epic and PR content. + + Run one or more dotCMS AI semantic searches to assess current documentation + coverage. Use the Bash tool to execute each search: + + curl -s -X POST "https://cdn.dotcms.dev/api/v1/ai/search" \ + -H "Authorization: Bearer $AUTODOC_DOTCMS_API_TOKEN_AISEARCH" \ + -H "Content-Type: application/json" \ + -H "Origin: https://dev.dotcms.com" \ + -H "Referer: https://dev.dotcms.com/" \ + -d '{"model":"gpt-5.2","indexName":"default","prompt":"","operator":"cosine","threshold":".25","searchLimit":20}' + + Run multiple searches with varied queries to thoroughly assess coverage. Use the + results to determine whether documentation exists, is sufficient, or needs updating. + + Then write the report to the path shown at the bottom of this context block, + using the Write tool. + + **Every report, regardless of outcome, must contain a machine-readable metadata + block placed immediately after the report header and before the `## Determination` + section.** Use exactly this structure and no other: + + ``` + + action: none + + ``` + + For `update` reports, add `urlTitle` (the urlTitle of the page being updated): + + ``` + + action: update + urlTitle: saml-authentication + + ``` + + For `create` reports, add `urlTitle` (proposed), `title`, `tags` + (comma-separated), and `seoDescription`: + + ``` + + action: create + urlTitle: vanity-urls-s3-static-publishing + title: Vanity URLs in S3 Static Publishing + tags: static publishing, push publishing, vanity urls, AWS S3 + seoDescription: Learn how to enable and configure opt-in Vanity URL materialization for AWS S3 static publishing in dotCMS. + + ``` + + The style of report depends upon a determination that must be made here: + + - There is a chance that no documentation change is warranted. This is common when + the Epic delivers internal architecture work, bug fixes, or a feature that is + already fully documented. Some cases will be clear and some uncertain — evaluate + whether the Epic's delivery is both end-user relevant and documentable. If no + documentation is needed, the report must explain why. No `` + token appears anywhere in no-documentation reports. + + - There is a chance that relevant documentation exists but needs an update. If the + docs are not fully current with what the Epic delivered, the report should first + present a differential explaining what changes are necessary and where. After all + analysis, the report must end with the token `` on its + own line, immediately followed by the **complete, final content of the page's + `documentation` field** — the full existing document with all proposed changes + already incorporated. This is not a diff and not an insertion fragment: it is the + entire field value as it should exist after the update, ready to be submitted to + the API as-is. The draft runs to the end of the file with no trailing content + after it. + + - There is a chance that the Epic introduces something documentable with no + existing coverage. In this case, the report should include a differential on the + documentation gap, a list of appropriate tags, a short SEO description, a + description of what feature this documentation should be grouped with, and what + user personas it is most relevant to. After all analysis, the report must end + with the token `` on its own line, immediately followed + by the complete draft of the new documentation page. The draft runs to the end + of the file with no trailing content after it. + + When drafting the documentation page, use horizontal rules (`---`) sparingly. + Headings already provide clear visual and structural separation; `---` should + be reserved only for the most stark structural breaks and must not appear + between ordinary subsections. + PROMPTEOF + - name: Build eval context run: | cat > /tmp/ctx.py << 'PYEOF' @@ -176,8 +271,8 @@ jobs: else: print(f'warning: no vault file for Epic #{epic_num}', file=sys.stderr) - prompt = open('dotcms-aios/autodoc/prompts/burlap.txt').read().strip() - report_path = f'dotcms-aios/autodoc/reports/Epic-{epic_num}_burlap.md' + prompt = open('/tmp/burlap.txt').read().strip() + report_path = f'/tmp/Epic-{epic_num}_burlap.md' lines += [ '', '---', '', '## Prompt: burlap', '', prompt, '', '---', '', @@ -196,7 +291,7 @@ jobs: - name: Post comment, apply to dotCMS, commit report run: | - REPORT="dotcms-aios/autodoc/reports/Epic-${EPIC_NUMBER}_burlap.md" + REPORT="/tmp/Epic-${EPIC_NUMBER}_burlap.md" if [ ! -f "$REPORT" ]; then echo "No report at $REPORT — skipping finalize." From e38fbd0f832b42ca7da09f3dfe6fb379b3005cc8 Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Wed, 24 Jun 2026 13:12:50 -0400 Subject: [PATCH 8/8] fix(autodoc): add source material quality check; restore details block - Add conservatism clause to prompt: if Epic lacks technical detail, set action none rather than hallucinating a draft - Restore
wrapping of doc draft in issue comment (was lost when post_report.py was replaced with inline shell) --- .github/workflows/issue_autodoc.yml | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue_autodoc.yml b/.github/workflows/issue_autodoc.yml index 506bcebf990..c1964f29ca5 100644 --- a/.github/workflows/issue_autodoc.yml +++ b/.github/workflows/issue_autodoc.yml @@ -118,6 +118,14 @@ jobs: Run multiple searches with varied queries to thoroughly assess coverage. Use the results to determine whether documentation exists, is sufficient, or needs updating. + **Source material quality check:** before drafting anything, assess whether the + Epic and its PRs contain enough real technical detail to write accurate + documentation. If the Epic is a placeholder, has no associated PRs, or describes + a feature without specifying its behaviour, configuration, or user-facing surface — + set `action: none` and explain that the source material is insufficient. Do not + infer, invent, or pad from general knowledge. A short honest `none` report is + always preferable to a hallucinated draft. + Then write the report to the path shown at the bottom of this context block, using the Write tool. @@ -298,8 +306,24 @@ jobs: exit 0 fi - # Prepend idempotency marker so subsequent runs can find and edit this comment. - { echo ''; cat "$REPORT"; } > /tmp/comment_body.md + # Build comment body: prepend idempotency marker and collapse the doc draft + # under a
block so the comment isn't a wall of text. + export REPORT + python3 << 'PYEOF' + import os + report = open(os.environ['REPORT']).read() + marker = '' + if marker in report: + before, draft = report.split(marker, 1) + body = (before.rstrip() + + '\n\n
\nDocumentation Draft\n\n' + + draft.lstrip('\n') + + '\n
') + else: + body = report + with open('/tmp/comment_body.md', 'w') as f: + f.write('\n' + body.rstrip()) + PYEOF # Edit the existing autodoc comment if one exists, otherwise create a new one. # --paginate ensures we search all comments, not just the first page.