From 8826d41f8b8c64dbca761e46dfdc26461f921357 Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Tue, 23 Jun 2026 16:33:06 -0400 Subject: [PATCH 01/12] 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 000000000000..af0e4f595618 --- /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 02/12] 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 af0e4f595618..a8a5c9db9017 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 03/12] 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 a8a5c9db9017..3737168454b9 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 04/12] 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 3737168454b9..5128ed3fb9c4 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 05/12] 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 5128ed3fb9c4..42fbcf70fc9c 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 06/12] 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 42fbcf70fc9c..89777c919569 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 07/12] 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 89777c919569..506bcebf9901 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 08/12] 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 506bcebf9901..c1964f29ca58 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. From 779a03245958d8812dafbba9501d0b57180089f4 Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Fri, 26 Jun 2026 08:40:31 -0400 Subject: [PATCH 09/12] =?UTF-8?q?fix(autodoc):=20address=20code=20review?= =?UTF-8?q?=20=E2=80=94=20Epic=20type,=20Bedrock=20model,=20step=20split?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace label check for Epic with issue type check (github.event.issue.type.name) - Update GraphQL parent lookup to use issueType { name } instead of labels - Add id-token: write permission for Bedrock OIDC - Replace npm install + claude --print with anthropics/claude-code-action@v1; model and Bedrock role sourced from vars.BEDROCK_MODEL_ID / vars.BEDROCK_ROLE_ARN - Split finalize into three discrete steps for easier auditing: Post report comment / Apply doc update to dotCMS / Apply doc create to dotCMS --- .github/workflows/issue_autodoc.yml | 147 +++++++++++++++++++--------- 1 file changed, 99 insertions(+), 48 deletions(-) diff --git a/.github/workflows/issue_autodoc.yml b/.github/workflows/issue_autodoc.yml index c1964f29ca58..486853f44370 100644 --- a/.github/workflows/issue_autodoc.yml +++ b/.github/workflows/issue_autodoc.yml @@ -8,7 +8,7 @@ jobs: preflight: name: Determine whether to run if: | - contains(github.event.issue.labels.*.name, 'Epic') || + github.event.issue.type.name == 'Epic' || contains(github.event.issue.labels.*.name, 'Doc : Needs Doc') || contains(github.event.issue.labels.*.name, 'Changelog: Needs Doc') runs-on: ubuntu-latest @@ -24,21 +24,21 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE_NUMBER: ${{ github.event.issue.number }} REPO: ${{ github.repository }} - HAS_EPIC: ${{ contains(github.event.issue.labels.*.name, 'Epic') }} + HAS_EPIC: ${{ github.event.issue.type.name == 'Epic' }} run: | if [ "$HAS_EPIC" = "true" ]; then - echo "Issue #$ISSUE_NUMBER has Epic label — proceeding unconditionally." + echo "Issue #$ISSUE_NUMBER has Epic type — proceeding unconditionally." echo "should_run=true" >> "$GITHUB_OUTPUT" exit 0 fi - PARENT_LABELS=$(gh api graphql \ + PARENT_TYPE=$(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 } } + issueType { name } } } } @@ -47,9 +47,9 @@ jobs: -f owner="${REPO%/*}" \ -f repo="${REPO#*/}" \ -F number="$ISSUE_NUMBER" \ - --jq '(.data.repository.issue.parent.labels.nodes // []) | .[].name') + --jq '.data.repository.issue.parent.issueType.name // ""') - if echo "$PARENT_LABELS" | grep -qx "Epic"; then + if [ "$PARENT_TYPE" = "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 @@ -65,6 +65,7 @@ jobs: permissions: issues: write contents: read + id-token: write env: EPIC_NUMBER: ${{ github.event.issue.number }} @@ -88,10 +89,12 @@ jobs: token: ${{ secrets.AUTODOC_AIOS_CI }} 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: Configure AWS credentials (Bedrock) + if: vars.BEDROCK_ROLE_ARN != '' + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ vars.BEDROCK_ROLE_ARN }} + aws-region: ${{ vars.BEDROCK_AWS_REGION || 'us-east-1' }} - name: Write prompt run: | @@ -294,15 +297,43 @@ jobs: PYEOF python3 /tmp/ctx.py - - name: Run Claude - run: claude --print --allowedTools Bash,Write < /tmp/eval_context.md + - name: Load eval context + id: eval_context + run: | + { + echo 'content<<__AUTODOC_EOF__' + cat /tmp/eval_context.md + echo '__AUTODOC_EOF__' + } >> "$GITHUB_OUTPUT" + + - name: Compose Claude args + id: claude_args + env: + BEDROCK_MODEL_ID: ${{ vars.BEDROCK_MODEL_ID }} + run: | + ARGS="--allowedTools Bash,Write" + if [ -n "$BEDROCK_MODEL_ID" ]; then + ARGS="--model $BEDROCK_MODEL_ID $ARGS" + fi + echo "value=$ARGS" >> "$GITHUB_OUTPUT" - - name: Post comment, apply to dotCMS, commit report + - name: Run Claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + use_bedrock: ${{ vars.BEDROCK_ROLE_ARN != '' && 'true' || 'false' }} + prompt: ${{ steps.eval_context.outputs.content }} + claude_args: ${{ steps.claude_args.outputs.value }} + use_sticky_comment: 'false' + track_progress: 'false' + + - name: Post report comment run: | REPORT="/tmp/Epic-${EPIC_NUMBER}_burlap.md" if [ ! -f "$REPORT" ]; then echo "No report at $REPORT — skipping finalize." + echo "ACTION=none" >> "$GITHUB_ENV" exit 0 fi @@ -336,29 +367,39 @@ jobs: gh issue comment "$EPIC_NUMBER" --repo "$REPO" --body-file /tmp/comment_body.md fi - # Parse action and urlTitle from the machine-readable meta block. - # All fields use sub() to preserve the full value after the key prefix. + # Parse action and urlTitle from the machine-readable meta block and export + # for the downstream apply steps. 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") + echo "ACTION=${ACTION:-none}" >> "$GITHUB_ENV" + echo "URL_TITLE=$URL_TITLE" >> "$GITHUB_ENV" + + - name: Apply doc update to dotCMS + if: env.ACTION == 'update' + run: | + REPORT="/tmp/Epic-${EPIC_NUMBER}_burlap.md" # 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 + if ! 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." + exit 0 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" \ - -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') + 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}" \ + | jq -r '(.esresponse[0].hits.hits[0]._source.identifier) // empty') + + # 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}$'; then + echo "No valid contentlet found for urlTitle '${URL_TITLE}' — skipping." + exit 0 + fi - # 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' + export REPORT IDENTIFIER + python3 << 'PYEOF' import json, os report = open(os.environ['REPORT']).read() marker = '' @@ -369,19 +410,29 @@ jobs: 'disabledWYSIWYG': ['documentation']}} json.dump(payload, open('/tmp/payload.json', 'w')) PYEOF - 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" \ - --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' + + 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" \ + --data @/tmp/payload.json + + - name: Apply doc create to dotCMS + if: env.ACTION == 'create' + run: | + REPORT="/tmp/Epic-${EPIC_NUMBER}_burlap.md" + + # Validate urlTitle is a URL slug before interpolating into the JSON query. + if ! 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." + exit 0 + fi + + 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 = '' @@ -399,10 +450,10 @@ jobs: payload['contentlet']['navFolder'] = sf json.dump(payload, open('/tmp/payload.json', 'w')) PYEOF - 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" \ - --data @/tmp/payload.json - fi + + 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" \ + --data @/tmp/payload.json From 2545e884ec06f17a7ca64b0d4c82129bda7c33db Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Fri, 26 Jun 2026 09:16:58 -0400 Subject: [PATCH 10/12] fix(binary-field): correct writting typo in Binary Field settings dialog; not worth its own PR. --- dotCMS/src/main/webapp/WEB-INF/messages/Language.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 4c223be630e6..645eff31edbf 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -6291,7 +6291,7 @@ editpage.locked-contact-with=Locked. Contact with {0} user binary-field.settings.allow.type=Allowed File Type binary-field.settings.system.options.allow.url.import=Allow users to create a file by importing from URL -binary-field.settings.system.options.allow.code.write=Allow users to create a file by writting code +binary-field.settings.system.options.allow.code.write=Allow users to create a file by writing code binary-field.settings.system.options.allow.generate.img=Allow users to create a file by AI binary-field.settings.system.options.allow.file.name.edit=Allow specifying the file name in the code editor binary-field-settings.system.link.to.documentation=Link to documentation From 7694f8c6cf6de43700f4b2120c75354b259dd37e Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Fri, 26 Jun 2026 13:59:10 -0400 Subject: [PATCH 11/12] ci(autodoc): replace AUTODOC_AIOS_CI PAT with CI_MACHINE_TOKEN for dotcms-aios checkout Co-Authored-By: Claude Sonnet 4.6 --- .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 486853f44370..0517208b5096 100644 --- a/.github/workflows/issue_autodoc.yml +++ b/.github/workflows/issue_autodoc.yml @@ -86,7 +86,7 @@ jobs: uses: actions/checkout@v4 with: repository: dotCMS/dotcms-aios - token: ${{ secrets.AUTODOC_AIOS_CI }} + token: ${{ secrets.CI_MACHINE_TOKEN }} path: dotcms-aios - name: Configure AWS credentials (Bedrock) From cc7e80f836bbca54843d5357790da7868f7d1993 Mon Sep 17 00:00:00 2001 From: Jamie Mauro Date: Mon, 29 Jun 2026 13:14:33 -0400 Subject: [PATCH 12/12] fix(javadocs): to in javadoc example; doc change not worth own PR. --- .../velocity/viewtools/OwaspEncoderTool.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/OwaspEncoderTool.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/OwaspEncoderTool.java index 9787e423c576..82d881b9eae8 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/OwaspEncoderTool.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/OwaspEncoderTool.java @@ -12,19 +12,19 @@ import org.owasp.encoder.Encode; /** - * Velocity view tool ({@code $encode}) that exposes the full + * Velocity view tool ({@code $owasp}) that exposes the full * OWASP Java Encoder API * directly to Velocity templates, covering every output context: HTML, HTML attributes, * JavaScript, CSS, URI components, and XML. * - *

Registered in {@code toolbox.xml} under the key {@code encode}. + *

Registered in {@code toolbox.xml} under the key {@code owasp}. * *

Example Velocity usage: *

- *   <p>$encode.forHtml($request.getParameter("name"))</p>
- *   <a href="/search?q=$encode.forUriComponent($request.getParameter("q"))">Go</a>
- *   <script>var msg = "$encode.forJavaScript($message)";</script>
- *   <div style="color: $encode.forCssString($color)">...</div>
+ *   <p>$owasp.forHtml($request.getParameter("name"))</p>
+ *   <a href="/search?q=$owasp.forUriComponent($request.getParameter("q"))">Go</a>
+ *   <script>var msg = "$owasp.forJavaScript($message)";</script>
+ *   <div style="color: $owasp.forCssString($color)">...</div>
  * 
* * @see com.liferay.util.Xss