# .github/workflows/docs-needed.yml # # Place this file in ALL of: # mattermost/mattermost -> .github/workflows/docs-needed.yml # mattermost/mattermost-mobile -> .github/workflows/docs-needed.yml # mattermost/desktop -> .github/workflows/docs-needed.yml # # Authentication — reuses the two-app pattern from changelog automation: # # Read app (vars.CHANGELOG_READ_CLIENT_ID / secrets.CHANGELOG_READ_PRIVATE_KEY) # Installed on mattermost/mattermost with contents: read. # Used only to look up milestones (step 3). # # Write app (vars.CHANGELOG_WRITE_CLIENT_ID / secrets.CHANGELOG_WRITE_PRIVATE_KEY) # Installed on mattermost/docs with contents: write + pull-requests: write. # Used to push the docs branch, open the draft PR, and manage milestones/reviewers. # changelog-automation-docs[bot] must be on the CLA ignore list in mattermost/docs. # # Other required secrets (set in each repo's Settings -> Secrets and Variables): # secrets.ANTHROPIC_API_KEY - Claude API key for doc generation # vars.CHANGELOG_GIT_USERNAME - Git display name for automated commits # vars.CHANGELOG_GIT_EMAIL - Git email for automated commits # # Behaviour: # When Docs/Needed is added to a non-draft PR from within the same repo, # opens one docs PR in mattermost/docs: # Branch: docs/-pr- # e.g. docs/mattermost-pr-1234, docs/desktop-pr-42 # Targets the versioned release branch (e.g. v11.7-documentation) if it # exists in mattermost/docs; falls back to master with a warning. # Draft doc generated by Claude using the draft_docs.md prompt. # Milestone on the docs PR mirrors the dev PR milestone. # Dev PR author added as reviewer on the docs PR. # Comment posted on the dev PR linking to the new docs PR. # If Docs/Needed is re-applied after removal, a fresh run creates or # updates the same branch (idempotent via the exists check). name: "docs/needed - Open Docs PR" on: pull_request: types: [labeled] workflow_dispatch: inputs: pr_number: description: >- PR number to process. Intended use cases: (1) smoke-test the workflow after merging without having to apply a label; (2) re-run after a transient failure (e.g. Claude API timeout, push error) without bouncing the Docs/Needed label; (3) validate the workflow against a known PR during development or review. required: true type: string # One run at a time per dev PR prevents a race if the label bounces quickly. # cancel-in-progress is false because a mid-run cancellation can leave the # docs branch in a partial state (e.g. commit pushed but PR not yet opened). # Queuing is safer; the idempotency checks in each step handle duplicates. concurrency: group: >- docs-needed-${{ github.repository }}-${{ github.event.pull_request.number || inputs.pr_number }} cancel-in-progress: false jobs: open-docs-pr: name: "Open Docs PR" runs-on: ubuntu-latest # Fork guard: only run on PRs opened from within this repo, not forks. # Draft guard: skip draft PRs - apply Docs/Needed when the PR is ready. # workflow_dispatch bypasses both guards for manual testing. if: | github.event_name == 'workflow_dispatch' || (github.event.label.name == 'Docs/Needed' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.draft == false) permissions: contents: read pull-requests: write steps: # 0. Generate short-lived installation tokens — one per app. # Tokens expire automatically after 1 hour and are not tied to any # individual user account. # # read-token: scoped to mattermost/mattermost for milestone lookups # write-token: scoped to mattermost/docs for branch push, PR creation, # milestone sync, and reviewer assignment - name: Generate read token id: read-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.CHANGELOG_READ_CLIENT_ID }} private-key: ${{ secrets.CHANGELOG_READ_PRIVATE_KEY }} repositories: mattermost - name: Generate write token id: write-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.CHANGELOG_WRITE_CLIENT_ID }} private-key: ${{ secrets.CHANGELOG_WRITE_PRIVATE_KEY }} repositories: docs # 0b. Resolve PR data — normalises pull_request event fields and # workflow_dispatch inputs into one set of outputs so all subsequent # steps are trigger-agnostic. For workflow_dispatch the PR is fetched # by number from this repo using the built-in GITHUB_TOKEN. - name: Resolve PR data id: pr-data env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} INPUT_PR_NUMBER: ${{ inputs.pr_number }} EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} EVENT_MILESTONE_NUMBER: ${{ github.event.pull_request.milestone.number }} EVENT_MILESTONE_TITLE: ${{ github.event.pull_request.milestone.title }} EVENT_MILESTONE_DUE: ${{ github.event.pull_request.milestone.due_on }} EVENT_PR_TITLE: ${{ github.event.pull_request.title }} EVENT_PR_AUTHOR: ${{ github.event.pull_request.user.login }} run: | if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then DATA=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$INPUT_PR_NUMBER") echo "pr_number=$INPUT_PR_NUMBER" >> "$GITHUB_OUTPUT" echo "milestone_number=$(echo "$DATA" | jq -r '.milestone.number // ""')" >> "$GITHUB_OUTPUT" echo "milestone_title=$(echo "$DATA" | jq -r '.milestone.title // ""')" >> "$GITHUB_OUTPUT" echo "milestone_due=$(echo "$DATA" | jq -r '.milestone.due_on // ""')" >> "$GITHUB_OUTPUT" echo "pr_title=$(echo "$DATA" | jq -r '.title')" >> "$GITHUB_OUTPUT" echo "pr_author=$(echo "$DATA" | jq -r '.user.login')" >> "$GITHUB_OUTPUT" else echo "pr_number=$EVENT_PR_NUMBER" >> "$GITHUB_OUTPUT" echo "milestone_number=$EVENT_MILESTONE_NUMBER" >> "$GITHUB_OUTPUT" echo "milestone_title=$EVENT_MILESTONE_TITLE" >> "$GITHUB_OUTPUT" echo "milestone_due=$EVENT_MILESTONE_DUE" >> "$GITHUB_OUTPUT" echo "pr_title=$EVENT_PR_TITLE" >> "$GITHUB_OUTPUT" echo "pr_author=$EVENT_PR_AUTHOR" >> "$GITHUB_OUTPUT" fi # 1. Guard: milestone is required - name: Require milestone env: MILESTONE: ${{ steps.pr-data.outputs.milestone_number }} run: | if [ -z "$MILESTONE" ]; then echo "::error title=Missing milestone::Set a milestone on this PR before applying Docs/Needed." exit 1 fi # 2. Compute shared variables - name: Set variables id: vars env: MILESTONE_TITLE: ${{ steps.pr-data.outputs.milestone_title }} MILESTONE_NUMBER: ${{ steps.pr-data.outputs.milestone_number }} PR_NUMBER: ${{ steps.pr-data.outputs.pr_number }} PR_TITLE: ${{ steps.pr-data.outputs.pr_title }} PR_AUTHOR: ${{ steps.pr-data.outputs.pr_author }} SOURCE_REPO: ${{ github.repository }} run: | REPO_NAME="${SOURCE_REPO##*/}" BRANCH="docs/${REPO_NAME}-pr-${PR_NUMBER}" DOCS_FILE="source/about/drafts/${REPO_NAME}-pr-${PR_NUMBER}.md" echo "repo_name=$REPO_NAME" >> "$GITHUB_OUTPUT" echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" echo "docs_file=$DOCS_FILE" >> "$GITHUB_OUTPUT" echo "milestone_title=$MILESTONE_TITLE" >> "$GITHUB_OUTPUT" echo "milestone_number=$MILESTONE_NUMBER" >> "$GITHUB_OUTPUT" echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" echo "pr_title=$PR_TITLE" >> "$GITHUB_OUTPUT" echo "pr_author=$PR_AUTHOR" >> "$GITHUB_OUTPUT" echo "source_repo=$SOURCE_REPO" >> "$GITHUB_OUTPUT" # 3. Resolve the versioned base branch in mattermost/docs # mattermost/mattermost milestones (e.g. "v11.7.0") map directly to # the docs branch (e.g. "v11.7-documentation"). # mattermost-mobile and desktop use different versioning schemes, but # releases are coordinated with mattermost/mattermost by due date. # mobile v2.40.0 due 2025-03-15 == mattermost v11.7.0 due 2025-03-15 # => docs branch v11.7-documentation. - name: Resolve docs base branch id: base-branch env: GH_TOKEN: ${{ steps.read-token.outputs.token }} MILESTONE_TITLE: ${{ steps.vars.outputs.milestone_title }} MILESTONE_DUE: ${{ steps.pr-data.outputs.milestone_due }} REPO_NAME: ${{ steps.vars.outputs.repo_name }} run: | derive_branch_from_title() { local title="$1" local version version=$(echo "$title" | grep -oE 'v[0-9]+\.[0-9]+' | head -1) echo "$version" } if [ "$REPO_NAME" == "mattermost" ]; then VERSION=$(derive_branch_from_title "$MILESTONE_TITLE") if [ -z "$VERSION" ]; then echo "::warning::Could not parse vMAJOR.MINOR from milestone" \ "'${MILESTONE_TITLE}' - targeting master." echo "base=master" >> "$GITHUB_OUTPUT" exit 0 fi echo "Derived docs branch: ${VERSION}-documentation" else if [ -z "$MILESTONE_DUE" ] || [ "$MILESTONE_DUE" == "null" ]; then echo "::warning::Milestone '${MILESTONE_TITLE}' has no due date" \ "- cannot match to a mattermost/mattermost release. Targeting master." echo "base=master" >> "$GITHUB_OUTPUT" exit 0 fi MILESTONE_DATE="${MILESTONE_DUE%%T*}" MM_MILESTONE=$(gh api repos/mattermost/mattermost/milestones \ --paginate \ | jq -r --arg date "$MILESTONE_DATE" \ '.[] | select(((.due_on // "")[0:10]) == $date) | .title' \ | head -1) if [ -z "$MM_MILESTONE" ]; then echo "::warning::No mattermost/mattermost milestone found with" \ "due date '${MILESTONE_DUE}'. Targeting master." echo "base=master" >> "$GITHUB_OUTPUT" exit 0 fi VERSION=$(derive_branch_from_title "$MM_MILESTONE") if [ -z "$VERSION" ]; then echo "::warning::Could not parse vMAJOR.MINOR from matched" \ "milestone '${MM_MILESTONE}'. Targeting master." echo "base=master" >> "$GITHUB_OUTPUT" exit 0 fi echo "Matched '${MILESTONE_TITLE}' to '${MM_MILESTONE}'" \ "=> ${VERSION}-documentation" fi DOCS_BRANCH="${VERSION}-documentation" EXISTS=$(gh api "repos/mattermost/docs/branches/${DOCS_BRANCH}" \ --jq '.name' 2>/dev/null || echo "") if [ -n "$EXISTS" ]; then echo "base=$DOCS_BRANCH" >> "$GITHUB_OUTPUT" echo "Docs branch '${DOCS_BRANCH}' found." else echo "base=master" >> "$GITHUB_OUTPUT" echo "::warning::Branch '${DOCS_BRANCH}' not found in" \ "mattermost/docs - targeting master." \ "Ensure docs-branch-create.yml has run for this milestone." fi # 4. Check for an existing docs PR (idempotency) - name: Check for existing docs PR id: existing env: GH_TOKEN: ${{ steps.write-token.outputs.token }} BRANCH: ${{ steps.vars.outputs.branch }} run: | RESULT=$(gh pr list \ --repo mattermost/docs \ --head "$BRANCH" \ --state open \ --json number,url \ --jq 'first // empty') if [ -n "$RESULT" ]; then echo "exists=true" >> "$GITHUB_OUTPUT" echo "number=$(echo "$RESULT" | jq -r .number)" >> "$GITHUB_OUTPUT" echo "url=$(echo "$RESULT" | jq -r .url)" >> "$GITHUB_OUTPUT" EXISTING_URL=$(echo "$RESULT" | jq -r .url) echo "::notice::Docs PR already open at ${EXISTING_URL} - updating content only." else echo "exists=false" >> "$GITHUB_OUTPUT" fi # 5. Checkout docs repo # Pinned to a specific commit SHA for supply-chain security. # To update: find the SHA for the desired tag at # https://github.com/actions/checkout/releases - name: Checkout docs repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: mattermost/docs token: ${{ steps.write-token.outputs.token }} path: docs-repo fetch-depth: 1 - name: Configure git identity working-directory: docs-repo run: | git config user.name "${{ vars.CHANGELOG_GIT_USERNAME }}" git config user.email "${{ vars.CHANGELOG_GIT_EMAIL }}" - name: Set up branch working-directory: docs-repo env: BRANCH: ${{ steps.vars.outputs.branch }} BASE_BRANCH: ${{ steps.base-branch.outputs.base }} run: | if git ls-remote --exit-code origin "$BRANCH" > /dev/null 2>&1; then git fetch origin "$BRANCH" git checkout "$BRANCH" else git fetch origin "$BASE_BRANCH" git checkout -b "$BRANCH" "origin/$BASE_BRANCH" fi # 6. Fetch PR content for Claude - name: Fetch PR diff and description env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ steps.vars.outputs.pr_number }} SOURCE_REPO: ${{ steps.vars.outputs.source_repo }} run: | gh pr diff "$PR_NUMBER" --repo "$SOURCE_REPO" 2>/dev/null \ | head -c 20000 > /tmp/pr-diff.txt || true gh pr view "$PR_NUMBER" --repo "$SOURCE_REPO" \ --json body --jq '.body // ""' > /tmp/pr-body.txt # 7. Generate documentation draft via Claude # # System prompt is read from mattermost/docs at runtime so it can be # updated independently of this workflow file. # File location in mattermost/docs: .github/prompts/draft_docs.md # # Raw API call (not anthropics/claude-code-action) because: # - We supply a custom system prompt read from a file in the docs repo # - We HTML-escape untrusted PR content before insertion (security) # - We validate the response structure before writing any file # claude-code-action is designed for code-execution tasks and does not # support file-based system prompts or the output validation required # by the security model here. # # Model: claude-sonnet-4-6 balances quality and cost for doc drafting # and is consistent with other Mattermost docs automation workflows. - name: Generate documentation draft env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} PR_TITLE: ${{ steps.vars.outputs.pr_title }} PR_NUMBER: ${{ steps.vars.outputs.pr_number }} SOURCE_REPO: ${{ steps.vars.outputs.source_repo }} MILESTONE_TITLE: ${{ steps.vars.outputs.milestone_title }} run: | PROMPT_FILE="docs-repo/.github/prompts/draft_docs.md" if [ ! -f "$PROMPT_FILE" ]; then echo "::error::Prompt file not found: $PROMPT_FILE" echo "Add draft_docs.md to mattermost/docs at .github/prompts/draft_docs.md" exit 1 fi cp "$PROMPT_FILE" /tmp/system_prompt.txt # Untrusted PR content is HTML-escaped then wrapped in XML tags. # Escaping converts < > & to < > & so that an attacker # cannot inject a closing tag (e.g. ) to break out # of the data section and inject arbitrary instructions. escape_xml() { python3 -c \ 'import sys,html;print(html.escape(sys.stdin.read(),quote=False),end="")' } escape_xml < /tmp/pr-body.txt > /tmp/pr-body-safe.txt escape_xml < /tmp/pr-diff.txt > /tmp/pr-diff-safe.txt # Escape user-controlled metadata fields for the prompt. # PR title is set by the PR author; milestone title is set by # maintainers but escaped here for defence-in-depth. PR_TITLE_SAFE=$(printf '%s' "$PR_TITLE" | escape_xml) MILESTONE_SAFE=$(printf '%s' "$MILESTONE_TITLE" | escape_xml) PR_URL="https://github.com/${SOURCE_REPO}/pull/${PR_NUMBER}" { echo "" echo "PR URL: ${PR_URL}" echo "PR Title: ${PR_TITLE_SAFE}" echo "Source Repository: ${SOURCE_REPO}" echo "milestone.title: ${MILESTONE_SAFE}" echo "" echo "" echo "" cat /tmp/pr-body-safe.txt echo "" echo "" echo "" cat /tmp/pr-diff-safe.txt echo "" } > /tmp/user_message.txt PAYLOAD=$(jq -n \ --rawfile system /tmp/system_prompt.txt \ --rawfile content /tmp/user_message.txt \ '{ model: "claude-sonnet-4-6", max_tokens: 2048, system: $system, messages: [{ role: "user", content: $content }] }') # --max-time 60: fail fast if the API is unresponsive. # Capture the full response before piping so we can inspect it. RESPONSE=$(curl -sf --max-time 60 \ https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d "$PAYLOAD") || { echo "::error::Claude API call failed (timeout or HTTP error)." exit 1 } echo "$RESPONSE" | jq -r '.content[0].text' > /tmp/generated-docs.md # Fail immediately if the API returned null or an empty body. # Log only the error type and message from the JSON response - not # the full body - to avoid exposing API metadata in workflow logs. if [ ! -s /tmp/generated-docs.md ] || \ [ "$(cat /tmp/generated-docs.md)" = "null" ]; then ERROR_INFO=$(echo "$RESPONSE" | jq -r ' if .error then "\(.error.type): \(.error.message)" else "unexpected null or empty content" end' 2>/dev/null || echo "response could not be parsed") echo "::error::Claude API returned empty/null content. ${ERROR_INFO}" exit 1 fi # Validate the output matches the required structure defined in the # system prompt. Missing sections indicate the model was steered away # from its instructions - a signal of possible prompt injection. if ! grep -qF '=== CAPABILITY SUMMARY ===' /tmp/generated-docs.md; then echo "::error::Output missing CAPABILITY SUMMARY section." \ "Possible prompt injection. Aborting." exit 1 fi if ! grep -qF '=== DOCUMENTATION DRAFT ===' /tmp/generated-docs.md; then echo "::error::Output missing DOCUMENTATION DRAFT section." \ "Possible prompt injection. Aborting." exit 1 fi # 8. Write docs file - name: Write docs file working-directory: docs-repo env: DOCS_FILE: ${{ steps.vars.outputs.docs_file }} SOURCE_REPO: ${{ steps.vars.outputs.source_repo }} PR_NUMBER: ${{ steps.vars.outputs.pr_number }} PR_TITLE: ${{ steps.vars.outputs.pr_title }} PR_AUTHOR: ${{ steps.vars.outputs.pr_author }} run: | mkdir -p "$(dirname "$DOCS_FILE")" # Sanitize PR title for the Markdown heading to prevent XSS if the # docs are rendered as HTML (e.g. < > become < >). PR_TITLE_SAFE=$(printf '%s' "$PR_TITLE" | \ python3 -c \ 'import sys,html;print(html.escape(sys.stdin.read(),quote=False),end="")') { printf '# %s\n\n' "$PR_TITLE_SAFE" printf '> **Status:** Draft - auto-generated. Requires editorial review.\n' printf '> **Source:** [%s#%s](https://github.com/%s/pull/%s)\n' \ "$SOURCE_REPO" "$PR_NUMBER" "$SOURCE_REPO" "$PR_NUMBER" printf '> **Dev reviewer:** @%s\n\n' "$PR_AUTHOR" printf -- '---\n\n' cat /tmp/generated-docs.md } > "$DOCS_FILE" # 9. Commit and push - name: Commit and push working-directory: docs-repo env: BRANCH: ${{ steps.vars.outputs.branch }} DOCS_FILE: ${{ steps.vars.outputs.docs_file }} SOURCE_REPO: ${{ steps.vars.outputs.source_repo }} PR_NUMBER: ${{ steps.vars.outputs.pr_number }} PR_TITLE: ${{ steps.vars.outputs.pr_title }} run: | # Stage only the specific docs file - not the entire working tree. git add "$DOCS_FILE" if git diff --cached --quiet; then echo "No content changes - skipping commit." else # Sanitize PR title for commit message: PR_TITLE is author-controlled # and may contain newlines, quotes, or be very long. Strip control # characters and truncate to 72 chars (git convention). PR_TITLE_SHORT=$(printf '%s' "$PR_TITLE" | tr -d '\n\r' | cut -c1-72) # Use printf with a literal format string and positional arguments, # then pipe to -F - so the message is never interpolated into a # shell command string. This avoids any risk of shell metacharacters # in PR_TITLE_SHORT being evaluated. printf 'docs: draft for %s#%s - %s\n' \ "$SOURCE_REPO" "$PR_NUMBER" "$PR_TITLE_SHORT" \ | git commit -F - git push origin "$BRANCH" fi # 10. Sync milestone to mattermost/docs - name: Sync milestone id: docs-milestone env: GH_TOKEN: ${{ steps.write-token.outputs.token }} MILESTONE_TITLE: ${{ steps.vars.outputs.milestone_title }} run: | # Pipe to standalone jq with --arg to pass the title safely. # gh api does not support jq variable bindings directly. NUM=$(gh api 'repos/mattermost/docs/milestones?state=all' \ --paginate \ | jq -r --arg title "$MILESTONE_TITLE" \ '.[] | select(.title == $title) | .number' \ | head -1) if [ -z "$NUM" ]; then NUM=$(gh api repos/mattermost/docs/milestones \ --method POST \ -f title="$MILESTONE_TITLE" \ --jq '.number') echo "::notice::Created milestone '$MILESTONE_TITLE' (#$NUM) in mattermost/docs." fi echo "number=$NUM" >> "$GITHUB_OUTPUT" # 11a. Create docs PR - name: Create docs PR id: create-pr if: steps.existing.outputs.exists == 'false' env: GH_TOKEN: ${{ steps.write-token.outputs.token }} BRANCH: ${{ steps.vars.outputs.branch }} MILESTONE_TITLE: ${{ steps.vars.outputs.milestone_title }} SOURCE_REPO: ${{ steps.vars.outputs.source_repo }} PR_NUMBER: ${{ steps.vars.outputs.pr_number }} PR_TITLE: ${{ steps.vars.outputs.pr_title }} PR_AUTHOR: ${{ steps.vars.outputs.pr_author }} run: | PR_LINK="[${SOURCE_REPO}#${PR_NUMBER}]" PR_LINK="${PR_LINK}(https://github.com/${SOURCE_REPO}/pull/${PR_NUMBER})" BODY=$(printf '%s\n\n%s\n\n%s\n%s\n%s\n%s\n%s\n\n%s' \ "Documentation draft for ${PR_LINK}." \ "> Auto-generated by Claude - requires editorial review before merging." \ "| Field | Value |" \ "|---|---|" \ "| **Source PR** | ${PR_LINK} |" \ "| **Dev reviewer** | @${PR_AUTHOR} |" \ "| **Milestone** | ${MILESTONE_TITLE} |" \ "") # Sanitize PR title for the docs PR title: same rules as commit message. PR_TITLE_SHORT=$(printf '%s' "$PR_TITLE" | tr -d '\n\r' | cut -c1-72) # Open as a draft so a human must explicitly un-draft before merging. # This is the primary control against auto-committing injected content. # gh pr create --milestone takes a title string, not a numeric ID. URL=$(gh pr create \ --repo mattermost/docs \ --head "$BRANCH" \ --base "${{ steps.base-branch.outputs.base }}" \ --title "docs: ${PR_TITLE_SHORT}" \ --body "$BODY" \ --milestone "$MILESTONE_TITLE" \ --draft) echo "number=$(echo "$URL" | grep -oE '[0-9]+$')" >> "$GITHUB_OUTPUT" echo "url=$URL" >> "$GITHUB_OUTPUT" # 11b. Update existing docs PR milestone (re-trigger path) - name: Update existing docs PR if: steps.existing.outputs.exists == 'true' env: GH_TOKEN: ${{ steps.write-token.outputs.token }} DOCS_PR: ${{ steps.existing.outputs.number }} MILESTONE_TITLE: ${{ steps.vars.outputs.milestone_title }} run: | gh pr edit "$DOCS_PR" \ --repo mattermost/docs \ --milestone "$MILESTONE_TITLE" # 12. Resolve final docs PR reference - name: Resolve docs PR reference id: resolve run: | if [ "${{ steps.existing.outputs.exists }}" == "true" ]; then echo "number=${{ steps.existing.outputs.number }}" >> "$GITHUB_OUTPUT" echo "url=${{ steps.existing.outputs.url }}" >> "$GITHUB_OUTPUT" else echo "number=${{ steps.create-pr.outputs.number }}" >> "$GITHUB_OUTPUT" echo "url=${{ steps.create-pr.outputs.url }}" >> "$GITHUB_OUTPUT" fi # 13. Add dev PR author as reviewer - name: Add reviewer env: GH_TOKEN: ${{ steps.write-token.outputs.token }} PR_AUTHOR: ${{ steps.vars.outputs.pr_author }} run: | gh pr edit "${{ steps.resolve.outputs.number }}" \ --repo mattermost/docs \ --add-reviewer "$PR_AUTHOR" \ || echo "::warning::Could not add @${PR_AUTHOR} as reviewer" \ "- they may need docs repo access first." # 14. Comment on the dev PR - name: Comment on dev PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ steps.vars.outputs.pr_number }} SOURCE_REPO: ${{ steps.vars.outputs.source_repo }} DOCS_PR_NUMBER: ${{ steps.resolve.outputs.number }} DOCS_PR_URL: ${{ steps.resolve.outputs.url }} run: | if [ "${{ steps.existing.outputs.exists }}" == "true" ]; then ACTION="refreshed at" else ACTION="opened at" fi DOCS_LINK="[mattermost/docs#${DOCS_PR_NUMBER}](${DOCS_PR_URL})" COMMENT=$(printf '%s\n\n%s\n\n%s' \ "A documentation draft has been ${ACTION} ${DOCS_LINK}." \ "The docs PR is opened as a **draft**. Please review the AI-generated content, edit as needed, then mark it ready for review before merging." \ "The Docs/Needed label will be replaced with Docs/Done once the docs PR is merged or closed.") gh pr comment "$PR_NUMBER" \ --repo "$SOURCE_REPO" \ --body "$COMMENT"