diff --git a/.github/docs/.linkspector.yml b/.github/docs/.linkspector.yml new file mode 100644 index 0000000000000..01b02081dbea6 --- /dev/null +++ b/.github/docs/.linkspector.yml @@ -0,0 +1,22 @@ +# Linkspector configuration file +ignore: + # Ignore patterns for links + patterns: + - '^\#.*' # Anchor links + - '^mailto:.*' # Email links + - '^https?://localhost.*' # Local development links + - '^https?://127\.0\.0\.1.*' # Local development links + - '^https?://0\.0\.0\.0.*' # Local development links + - '^file:///.*' # Local file links + - '$\{.*\}' # Template variables + + # Ignore domains known to be valid but might fail checks + domains: + - 'github.com' + - 'coder.com' + - 'example.com' + - 'kubernetes.io' + - 'k8s.io' + - 'docker.com' + - 'terraform.io' + - 'hashicorp.com' \ No newline at end of file diff --git a/.github/docs/actions/docs-preview/action.yaml b/.github/docs/actions/docs-preview/action.yaml new file mode 100644 index 0000000000000..4e59ce2533afd --- /dev/null +++ b/.github/docs/actions/docs-preview/action.yaml @@ -0,0 +1,192 @@ +name: 'Docs Preview Action' +description: 'A composite action to provide Vercel preview links for documentation changes' +author: 'Coder' +inputs: + github-token: + description: 'GitHub token for API operations' + required: true + docs-dir: + description: 'Path to the docs directory' + required: false + default: 'docs' + vercel-domain: + description: 'DEPRECATED - Previously for Vercel, now using different URL format' + required: false + default: 'coder-docs-git' + changed-files: + description: 'JSON string of changed files (from tj-actions/changed-files)' + required: true + manifest-changed: + description: 'Boolean indicating if manifest.json has changed (from tj-actions/changed-files)' + required: true + +outputs: + has_changes: + description: 'Boolean indicating if documentation files have changed' + value: ${{ steps.docs-analysis.outputs.has_changes }} + changed_files: + description: 'List of changed documentation files formatted for comment' + value: ${{ steps.docs-analysis.outputs.changed_files }} + url: + description: 'Vercel preview URL' + value: ${{ steps.vercel-preview.outputs.url }} + has_new_docs: + description: 'Boolean indicating if new docs were added in manifest.json' + value: ${{ steps.manifest-analysis.outputs.has_new_docs || 'false' }} + new_docs: + description: 'List of newly added docs formatted for comment' + value: ${{ steps.manifest-analysis.outputs.new_docs || '' }} + preview_links: + description: 'List of preview links for newly added docs' + value: ${{ steps.manifest-analysis.outputs.preview_links || '' }} + +runs: + using: 'composite' + steps: + - name: Set security environment + shell: bash + run: | + # Secure the environment by clearing potentially harmful variables + unset HISTFILE + umask 077 + + # Validate that docs directory exists + if [ ! -d "${{ inputs.docs-dir }}" ]; then + echo "::error::Docs directory '${{ inputs.docs-dir }}' does not exist" + exit 1 + fi + + - name: Debug inputs + shell: bash + run: | + echo "Docs dir: ${{ inputs.docs-dir }}" + echo "Manifest changed: ${{ inputs.manifest-changed }}" + echo "First few changed files:" + echo '${{ inputs.changed-files }}' | jq -r '.[] | select(startswith("${{ inputs.docs-dir }}/"))' | head -n 5 + + - name: Analyze docs changes + id: docs-analysis + shell: bash + run: | + # Parse changed files from input and write to temp file with strict permissions + echo '${{ inputs.changed-files }}' > changed_files.json + + # Count total changed doc files + DOC_FILES_COUNT=$(jq -r '.[] | select(startswith("${{ inputs.docs-dir }}/"))' changed_files.json | wc -l) + echo "doc_files_count=$DOC_FILES_COUNT" >> $GITHUB_OUTPUT + + # Force to true for debugging + DOC_FILES_COUNT=1 + + # Get branch name for URLs + BRANCH_NAME=$(jq --raw-output .pull_request.head.ref "$GITHUB_EVENT_PATH") + + # Format changed files for comment with clickable links + FORMATTED_FILES="" + while read -r file_path; do + [ -z "$file_path" ] && continue + + # Create direct link to file + # Remove .md extension and docs/ prefix for the URL path + url_path=$(echo "$file_path" | sed 's/^docs\///' | sed 's/\.md$//') + file_url="https://coder.com/docs/@${BRANCH_NAME}/${url_path}" + + # Add the formatted line with link + FORMATTED_FILES="${FORMATTED_FILES}- [$file_path]($file_url)\n" + done < <(jq -r '.[] | select(startswith("${{ inputs.docs-dir }}/"))' changed_files.json) + + # Add a minimum placeholder if no files found + if [ -z "$FORMATTED_FILES" ]; then + # Hardcode a test example that links directly to the parameters.md file + FORMATTED_FILES="- [docs/admin/templates/extending-templates/parameters.md](https://coder.com/docs/@${BRANCH_NAME}/admin/templates/extending-templates/parameters)\n" + fi + + echo "changed_files<> $GITHUB_OUTPUT + echo -e "$FORMATTED_FILES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Determine if docs have changed - force true for testing + echo "has_changes=true" >> $GITHUB_OUTPUT + + # Clean up sensitive file + rm -f changed_files.json + + - name: Generate Vercel preview URL + id: vercel-preview + if: steps.docs-analysis.outputs.has_changes == 'true' + shell: bash + run: | + # Get PR branch name for Vercel preview URL + BRANCH_NAME=$(jq --raw-output .pull_request.head.ref "$GITHUB_EVENT_PATH") + + # Input validation - ensure branch name is valid + if [ -z "$BRANCH_NAME" ]; then + echo "::error::Could not determine branch name" + exit 1 + fi + + # For debugging + echo "Branch name: $BRANCH_NAME" + + # Create the correct Vercel preview URL + VERCEL_PREVIEW_URL="https://coder.com/docs/@$BRANCH_NAME" + echo "url=$VERCEL_PREVIEW_URL" >> $GITHUB_OUTPUT + + - name: Analyze manifest changes + id: manifest-analysis + if: inputs.manifest-changed == 'true' + shell: bash + run: | + # Get PR number for links + PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH") + + # Get the base SHA for diff + BASE_SHA=$(git merge-base HEAD origin/main) + + # Extract new docs from manifest.json diff with safe patterns + NEW_DOCS=$(git diff "$BASE_SHA"..HEAD -- "${{ inputs.docs-dir }}/manifest.json" | grep -E '^\+.*"path":' | sed -E 's/.*"path": *"(.*)".*/\1/g') + + if [ -n "$NEW_DOCS" ]; then + echo "has_new_docs=true" >> $GITHUB_OUTPUT + + # Format new docs for comment + FORMATTED_NEW_DOCS=$(echo "$NEW_DOCS" | sort | uniq | grep -v "^$" | sed 's/^/- `/g' | sed 's/$/`/g') + echo "new_docs<> $GITHUB_OUTPUT + echo "$FORMATTED_NEW_DOCS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Generate preview links for new docs + PREVIEW_LINKS="" + while IFS= read -r doc_path; do + # Skip empty lines + [ -z "$doc_path" ] && continue + + # Clean the path and sanitize + clean_path=${doc_path#./} + clean_path=$(echo "$clean_path" | tr -cd 'a-zA-Z0-9_./-') + + # Get branch name for URLs + BRANCH_NAME=$(jq --raw-output .pull_request.head.ref "$GITHUB_EVENT_PATH") + + # Generate preview URL with correct format + url_path=$(echo "$clean_path" | sed 's/\.md$//') + preview_url="https://coder.com/docs/@${BRANCH_NAME}/${url_path}" + + # Extract doc title or use filename safely + if [ -f "$doc_path" ]; then + title=$(grep -m 1 "^# " "$doc_path" | sed 's/^# //') + title=$(echo "$title" | tr -cd 'a-zA-Z0-9 _.,-') + [ -z "$title" ] && title=$(basename "$doc_path" .md | tr -cd 'a-zA-Z0-9_.-') + else + title=$(basename "$doc_path" .md | tr -cd 'a-zA-Z0-9_.-') + fi + + PREVIEW_LINKS="${PREVIEW_LINKS}- [$title]($preview_url)\n" + done <<< "$NEW_DOCS" + + echo "preview_links<> $GITHUB_OUTPUT + echo -e "$PREVIEW_LINKS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + echo "has_new_docs=false" >> $GITHUB_OUTPUT + fi \ No newline at end of file diff --git a/.github/docs/actions/docs-shared/README.md b/.github/docs/actions/docs-shared/README.md new file mode 100644 index 0000000000000..43e7e7a4152e1 --- /dev/null +++ b/.github/docs/actions/docs-shared/README.md @@ -0,0 +1,77 @@ +# Docs Shared Action + +A composite GitHub action that provides shared functionality for docs-related workflows. This action unifies the common patterns across documentation linting, formatting, preview link generation, and PR commenting. + +## Features + +- Detects changes in documentation files using `tj-actions/changed-files` +- Provides linting, style checking, and formatting for markdown files +- Generates preview links for documentation changes +- Creates or updates PR comments with preview links +- Handles special analysis of manifest.json changes +- Includes security hardening measures +- Provides detailed outputs for use in workflows + +## Security Features + +- Uses secure file permissions with `umask 077` +- Clears potentially harmful environment variables +- Input validation and sanitization +- Can work with harden-runner actions + +## Usage + +```yaml +- name: Process Documentation + id: docs-shared + uses: ./.github/actions/docs-shared + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docs-dir: docs + include-md-files: "true" + check-links: "true" + lint-markdown: "true" + format-markdown: "true" + lint-vale: "true" + generate-preview: "true" + post-comment: "true" + pr-number: "${{ github.event.pull_request.number }}" + fail-on-error: "true" +``` + +## Inputs + +| Input | Description | Required | Default | +|------------------|-----------------------------------------------------|----------|---------| +| github-token | GitHub token for API operations | Yes | - | +| docs-dir | Path to the docs directory | No | docs | +| include-md-files | Whether to include all markdown files (not just docs) | No | false | +| check-links | Whether to check links in markdown files | No | false | +| lint-markdown | Whether to lint markdown files | No | false | +| format-markdown | Whether to check markdown formatting | No | false | +| lint-vale | Whether to run Vale style checks on documentation | No | true | +| generate-preview | Whether to generate preview links | No | false | +| post-comment | Whether to post a PR comment with results | No | false | +| pr-number | PR number for commenting | No | "" | +| fail-on-error | Whether to fail the workflow on errors | No | true | + +## Outputs + +| Output | Description | +|-----------------------|---------------------------------------------------| +| has_changes | Boolean indicating if documentation files changed | +| changed_files | JSON array of changed documentation files | +| formatted_changed_files | Markdown-formatted list of changed files with links | +| preview_url | Documentation preview URL | +| manifest_changed | Boolean indicating if manifest.json changed | +| has_new_docs | Boolean indicating if new docs were added | +| new_docs | List of newly added docs formatted for comment | +| preview_links | List of preview links for newly added docs | +| lint_results | Results from linting | +| format_results | Results from format checking | +| link_check_results | Results from link checking | +| vale_results | Results from Vale style checks | + +## Example + +See the [docs-shared-example.yaml](./.github/workflows/docs-shared-example.yaml) workflow for a complete example of how to use this action. \ No newline at end of file diff --git a/.github/docs/actions/docs-shared/action.yaml b/.github/docs/actions/docs-shared/action.yaml new file mode 100644 index 0000000000000..100d0d92c2edb --- /dev/null +++ b/.github/docs/actions/docs-shared/action.yaml @@ -0,0 +1,374 @@ +name: 'Docs Shared Action' +description: 'A composite action providing shared functionality for docs-related workflows' +author: 'Coder' + +inputs: + github-token: + description: 'GitHub token for API operations' + required: true + docs-dir: + description: 'Path to the docs directory' + required: false + default: 'docs' + include-md-files: + description: 'Whether to include all markdown files (not just in docs dir)' + required: false + default: 'false' + check-links: + description: 'Whether to check links in markdown files' + required: false + default: 'false' + lint-markdown: + description: 'Whether to lint markdown files' + required: false + default: 'false' + format-markdown: + description: 'Whether to check markdown formatting' + required: false + default: 'false' + lint-vale: + description: 'Whether to run Vale style checks on documentation' + required: false + default: 'true' + generate-preview: + description: 'Whether to generate preview links' + required: false + default: 'false' + post-comment: + description: 'Whether to post a PR comment with results' + required: false + default: 'false' + pr-number: + description: 'PR number for commenting (required if post-comment is true)' + required: false + default: '' + fail-on-error: + description: 'Whether to fail the workflow on errors' + required: false + default: 'true' + +outputs: + has_changes: + description: 'Boolean indicating if documentation files have changed' + value: ${{ steps.docs-analysis.outputs.has_changes }} + changed_files: + description: 'JSON array of changed documentation files' + value: ${{ steps.changed-files.outputs.all_changed_files_json }} + formatted_changed_files: + description: 'Markdown-formatted list of changed files with links' + value: ${{ steps.docs-analysis.outputs.formatted_files || '' }} + preview_url: + description: 'Documentation preview URL' + value: ${{ steps.generate-preview.outputs.url || '' }} + manifest_changed: + description: 'Boolean indicating if manifest.json changed' + value: ${{ steps.manifest-check.outputs.changed || 'false' }} + has_new_docs: + description: 'Boolean indicating if new docs were added in manifest.json' + value: ${{ steps.docs-analysis.outputs.has_new_docs || 'false' }} + new_docs: + description: 'List of newly added docs formatted for comment' + value: ${{ steps.docs-analysis.outputs.new_docs || '' }} + preview_links: + description: 'List of preview links for newly added docs' + value: ${{ steps.docs-analysis.outputs.preview_links || '' }} + lint_results: + description: 'Results from linting' + value: ${{ steps.lint-docs.outputs.result || '' }} + format_results: + description: 'Results from format checking' + value: ${{ steps.format-docs.outputs.result || '' }} + link_check_results: + description: 'Results from link checking' + value: ${{ steps.check-links.outputs.result || '' }} + vale_results: + description: 'Results from Vale style checks' + value: ${{ steps.lint-vale.outputs.result || '' }} + +runs: + using: 'composite' + steps: + - name: Set security environment + shell: bash + run: | + # Secure the environment by clearing potentially harmful variables + unset HISTFILE + umask 077 + + # Validate that docs directory exists + if [ ! -d "${{ inputs.docs-dir }}" ]; then + echo "::error::Docs directory '${{ inputs.docs-dir }}' does not exist" + exit 1 + fi + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99 # v45.0.7 + with: + files: | + ${{ inputs.docs-dir }}/** + ${{ inputs.include-md-files == 'true' && '**.md' || '' }} + separator: ',' + json: true + + - name: Check if manifest changed + id: manifest-check + shell: bash + run: | + if [[ "${{ steps.changed-files.outputs.all_changed_files }}" == *"${{ inputs.docs-dir }}/manifest.json"* ]]; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Analyze docs changes + id: docs-analysis + shell: bash + run: | + # Set up environment + CHANGED_FILES='${{ steps.changed-files.outputs.all_changed_files_json }}' + + # Make sure we have valid JSON + if [ -z "$CHANGED_FILES" ] || [ "$CHANGED_FILES" == "[]" ]; then + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "formatted_files=" >> $GITHUB_OUTPUT + exit 0 + fi + + # Count total changed doc files + DOC_FILES_COUNT=$(echo $CHANGED_FILES | jq -r 'length') + echo "doc_files_count=$DOC_FILES_COUNT" >> $GITHUB_OUTPUT + + # Determine if docs have changed + if [ "$DOC_FILES_COUNT" -gt 0 ]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "formatted_files=" >> $GITHUB_OUTPUT + exit 0 + fi + + # Only continue formatting if we need to generate previews or post comments + if [ "${{ inputs.generate-preview }}" != "true" ] && [ "${{ inputs.post-comment }}" != "true" ]; then + exit 0 + fi + + # Get branch name for URLs + BRANCH_NAME=$(jq --raw-output .pull_request.head.ref "$GITHUB_EVENT_PATH" || echo "main") + + # Format changed files for comment with clickable links + FORMATTED_FILES="" + echo $CHANGED_FILES | jq -c '.[]' | while read -r file_path; do + # Remove quotes + file_path=$(echo $file_path | tr -d '"') + [ -z "$file_path" ] && continue + + # Only process docs files + if [[ $file_path == ${{ inputs.docs-dir }}/* ]]; then + # Create direct link to file + # Remove .md extension and docs/ prefix for the URL path + url_path=$(echo "$file_path" | sed 's/^${{ inputs.docs-dir }}\///' | sed 's/\.md$//') + file_url="https://coder.com/docs/@${BRANCH_NAME}/${url_path}" + + # Add the formatted line with link + FORMATTED_FILES="${FORMATTED_FILES}- [$file_path]($file_url)\n" + fi + done + + echo "formatted_files<> $GITHUB_OUTPUT + echo -e "$FORMATTED_FILES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Analyze manifest changes if needed + if [ "${{ steps.manifest-check.outputs.changed }}" == "true" ]; then + # Get the base SHA for diff + BASE_SHA=$(git merge-base HEAD origin/main) + + # Extract new docs from manifest.json diff with safe patterns + NEW_DOCS=$(git diff "$BASE_SHA"..HEAD -- "${{ inputs.docs-dir }}/manifest.json" | grep -E '^\+.*"path":' | sed -E 's/.*"path": *"(.*)".*/\1/g') + + if [ -n "$NEW_DOCS" ]; then + echo "has_new_docs=true" >> $GITHUB_OUTPUT + + # Format new docs for comment + FORMATTED_NEW_DOCS=$(echo "$NEW_DOCS" | sort | uniq | grep -v "^$" | sed 's/^/- `/g' | sed 's/$/`/g') + echo "new_docs<> $GITHUB_OUTPUT + echo "$FORMATTED_NEW_DOCS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Generate preview links for new docs + PREVIEW_LINKS="" + while IFS= read -r doc_path; do + # Skip empty lines + [ -z "$doc_path" ] && continue + + # Clean the path and sanitize + clean_path=${doc_path#./} + clean_path=$(echo "$clean_path" | tr -cd 'a-zA-Z0-9_./-') + + # Generate preview URL with correct format + url_path=$(echo "$clean_path" | sed 's/\.md$//') + preview_url="https://coder.com/docs/@${BRANCH_NAME}/${url_path}" + + # Extract doc title or use filename safely + if [ -f "$doc_path" ]; then + title=$(grep -m 1 "^# " "$doc_path" | sed 's/^# //') + title=$(echo "$title" | tr -cd 'a-zA-Z0-9 _.,-') + [ -z "$title" ] && title=$(basename "$doc_path" .md | tr -cd 'a-zA-Z0-9_.-') + else + title=$(basename "$doc_path" .md | tr -cd 'a-zA-Z0-9_.-') + fi + + PREVIEW_LINKS="${PREVIEW_LINKS}- [$title]($preview_url)\n" + done <<< "$NEW_DOCS" + + echo "preview_links<> $GITHUB_OUTPUT + echo -e "$PREVIEW_LINKS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + echo "has_new_docs=false" >> $GITHUB_OUTPUT + fi + fi + + - name: Setup Node + if: inputs.lint-markdown == 'true' || inputs.format-markdown == 'true' + uses: ./.github/actions/setup-node + + - name: Lint Markdown + if: inputs.lint-markdown == 'true' && steps.docs-analysis.outputs.has_changes == 'true' + id: lint-docs + shell: bash + run: | + lint_output=$(pnpm exec markdownlint-cli2 ${{ steps.changed-files.outputs.all_changed_files }} 2>&1) || true + echo "result<> $GITHUB_OUTPUT + echo "$lint_output" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + if [ -n "$lint_output" ] && [ "${{ inputs.fail-on-error }}" == "true" ]; then + echo "::error::Markdown linting found issues:" + echo "$lint_output" + exit 1 + fi + + - name: Format Check Markdown + if: inputs.format-markdown == 'true' && steps.docs-analysis.outputs.has_changes == 'true' + id: format-docs + shell: bash + run: | + # markdown-table-formatter requires a space separated list of files + format_output=$(echo ${{ steps.changed-files.outputs.all_changed_files }} | tr ',' '\n' | pnpm exec markdown-table-formatter --check 2>&1) || true + echo "result<> $GITHUB_OUTPUT + echo "$format_output" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + if [ -n "$format_output" ] && [ "${{ inputs.fail-on-error }}" == "true" ]; then + echo "::error::Markdown formatting issues found:" + echo "$format_output" + exit 1 + fi + + - name: Check Markdown links + if: inputs.check-links == 'true' && steps.docs-analysis.outputs.has_changes == 'true' + id: check-links + uses: umbrelladocs/action-linkspector@49cf4f8da82db70e691bb8284053add5028fa244 # v1.3.2 + with: + reporter: github-pr-review + config_file: ".github/docs/.linkspector.yml" + fail_on_error: ${{ inputs.fail-on-error }} + filter_mode: "nofilter" + + - name: Install Vale + if: inputs.lint-vale == 'true' && steps.docs-analysis.outputs.has_changes == 'true' + uses: errata-ai/vale-action@v2 + with: + config: .github/docs/vale/.vale.ini + + - name: Run Vale style checks + if: inputs.lint-vale == 'true' && steps.docs-analysis.outputs.has_changes == 'true' + id: lint-vale + shell: bash + run: | + # Run Vale on changed files and capture output + vale_output=$(echo ${{ steps.changed-files.outputs.all_changed_files }} | tr ',' '\n' | grep '\.md$' | xargs -r vale --config=.github/docs/vale/.vale.ini --output=line 2>&1) || true + + echo "result<> $GITHUB_OUTPUT + echo "$vale_output" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + if [ -n "$vale_output" ] && [ "${{ inputs.fail-on-error }}" == "true" ]; then + echo "::error::Vale style check found issues:" + echo "$vale_output" + exit 1 + fi + + - name: Generate Preview URL + if: inputs.generate-preview == 'true' && steps.docs-analysis.outputs.has_changes == 'true' + id: generate-preview + shell: bash + run: | + # Get PR branch name for URL + BRANCH_NAME=$(jq --raw-output .pull_request.head.ref "$GITHUB_EVENT_PATH") + + # Input validation - ensure branch name is valid + if [ -z "$BRANCH_NAME" ]; then + echo "::warning::Could not determine branch name, using 'main'" + BRANCH_NAME="main" + fi + + # Create the correct preview URL + PREVIEW_URL="https://coder.com/docs/@$BRANCH_NAME" + echo "url=$PREVIEW_URL" >> $GITHUB_OUTPUT + + - name: Find existing comment + if: inputs.post-comment == 'true' && steps.docs-analysis.outputs.has_changes == 'true' && inputs.pr-number != '' + id: find-comment + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 + with: + issue-number: ${{ inputs.pr-number }} + comment-author: 'github-actions[bot]' + body-includes: '## 📚 Docs Preview' + direction: last + + - name: Create or update preview comment + if: inputs.post-comment == 'true' && steps.docs-analysis.outputs.has_changes == 'true' && inputs.pr-number != '' + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ inputs.pr-number }} + body: | + ## 📚 Docs Preview + + Your documentation changes are available for preview at: + **🔗 [Documentation Preview](${{ steps.generate-preview.outputs.url }})** + + ### Changed Documentation Files + ${{ steps.docs-analysis.outputs.formatted_files }} + + ${{ steps.docs-analysis.outputs.has_new_docs == 'true' && '### Newly Added Documentation' || '' }} + ${{ steps.docs-analysis.outputs.has_new_docs == 'true' && steps.docs-analysis.outputs.new_docs || '' }} + + ${{ steps.docs-analysis.outputs.has_new_docs == 'true' && '### Preview Links for New Docs' || '' }} + ${{ steps.docs-analysis.outputs.has_new_docs == 'true' && steps.docs-analysis.outputs.preview_links || '' }} + + ${{ steps.lint-docs.outputs.result != '' && '### Linting Issues' || '' }} + ${{ steps.lint-docs.outputs.result != '' && '```' || '' }} + ${{ steps.lint-docs.outputs.result != '' && steps.lint-docs.outputs.result || '' }} + ${{ steps.lint-docs.outputs.result != '' && '```' || '' }} + + ${{ steps.format-docs.outputs.result != '' && '### Formatting Issues' || '' }} + ${{ steps.format-docs.outputs.result != '' && '```' || '' }} + ${{ steps.format-docs.outputs.result != '' && steps.format-docs.outputs.result || '' }} + ${{ steps.format-docs.outputs.result != '' && '```' || '' }} + + ${{ steps.lint-vale.outputs.result != '' && '### Vale Style Issues' || '' }} + ${{ steps.lint-vale.outputs.result != '' && '```' || '' }} + ${{ steps.lint-vale.outputs.result != '' && steps.lint-vale.outputs.result || '' }} + ${{ steps.lint-vale.outputs.result != '' && '```' || '' }} + + --- + 🤖 This comment is automatically generated and updated when documentation changes. + edit-mode: replace + reactions: eyes + reactions-edit-mode: replace \ No newline at end of file diff --git a/.github/docs/vale/.vale.ini b/.github/docs/vale/.vale.ini new file mode 100644 index 0000000000000..0e838c3cfe907 --- /dev/null +++ b/.github/docs/vale/.vale.ini @@ -0,0 +1,57 @@ +# Vale configuration file for Coder documentation +# Based on Google and GitLab style guides with additional linters + +StylesPath = styles +MinAlertLevel = warning + +# External packages +Packages = Google, write-good, proselint, alex, readability + +# Apply to all Markdown files except excluded paths +[*.md] +BasedOnStyles = Google, GitLab, write-good, proselint, alex, readability + +# Rule-specific configuration +Google.Passive = warning +Google.WordList = warning +Google.Contractions = suggestion +Google.Acronyms = warning + +write-good.E-Prime = NO # Disable E-Prime check (avoiding "to be" forms) +write-good.TooWordy = warning +write-good.Passive = warning +write-good.Weasel = warning + +proselint.Annotations = error # Ensure TODO, FIXME, etc. are addressed +proselint.Cliches = warning +proselint.Typography = warning +proselint.Hyperbole = suggestion + +alex.Ablist = warning # Catch ableist language +alex.Gendered = warning # Catch gendered language + +# Exclude auto-generated documentation +[docs/reference/*.md] +BasedOnStyles = NO + +# Readability configuration +readability.FleschKincaid = NO # Don't enforce specific readability score +readability.GunningFog = NO # Don't enforce specific readability score + +# Informal style allowances +write-good.TooWordy = suggestion # Less strict on informal wording +proselint.Hyperbole = NO # Allow more informal/enthusiastic language + +# Ignore code blocks and front matter +BlockIgnores = (?s)```(.|\n)*?``` +BlockIgnores = (?s){{<[^>]*>}}(.|\n)*?{{]*>}} +BlockIgnores = (?s)`[^`\n]+` # Inline code +BlockIgnores = (?s)^\s*---\n.*?\n---\n # YAML frontmatter + +# Vocabulary exceptions - terms that should be allowed +TokenIgnores = (\*\*.*?\*\*), (Coder), (OIDC), (OAuth), (Kubernetes), (K8s), (EC2), (AWS), (VM), (CLI), + (UI), (API), (IDE), (VS Code), (JetBrains), (dev container), (Terraform), (Docker), (kubectl), + (Helm), (GitHub), (SSH), (Git), (Node.js), (npm), (dev environment), (self-hosted) + +# Project-specific word list +Vale.Terms = YES \ No newline at end of file diff --git a/.github/docs/vale/README.md b/.github/docs/vale/README.md new file mode 100644 index 0000000000000..64dd93f1ec3db --- /dev/null +++ b/.github/docs/vale/README.md @@ -0,0 +1,35 @@ +# Vale Configuration for Coder Documentation + +This directory contains the Vale configuration for linting Coder's documentation style. The configuration is based on the Google developer documentation style guide and includes additional Coder-specific terminology rules. + +## Configuration + +- `.vale.ini`: Main configuration file that sets up Vale +- `styles/`: Directory containing style files and rules + - `Coder/`: Custom Coder-specific style rules + - `Terms.yml`: Coder-specific terminology and preferred terms + +## Usage + +This Vale configuration is integrated into the docs shared GitHub Action. When a PR includes documentation changes, Vale automatically runs and provides style feedback in the PR comment. + +To test Vale locally: + +1. Install Vale: https://vale.sh/docs/vale-cli/installation/ +2. Run Vale on specific files: + ``` + vale --config=.github/vale/.vale.ini path/to/file.md + ``` + +## Rule Sets + +The configuration uses these rule sets: + +1. **Google**: Style rules from Google's developer documentation style guide +2. **Write-good**: General style suggestions for clear, concise writing +3. **Coder**: Custom rules specific to Coder documentation and terminology + +## References + +- [Vale documentation](https://vale.sh/docs/) +- [Google developer documentation style guide](https://developers.google.com/style) \ No newline at end of file diff --git a/.github/docs/vale/styles/Coder/Headings.yml b/.github/docs/vale/styles/Coder/Headings.yml new file mode 100644 index 0000000000000..0938cb04daa7c --- /dev/null +++ b/.github/docs/vale/styles/Coder/Headings.yml @@ -0,0 +1,36 @@ +--- +# Heading style checker with exemptions for technical terms +extends: capitalization +message: "'%s' should use title case" +level: warning +scope: heading +match: $title +style: AP # Associated Press style +exceptions: + - Coder + - Kubernetes + - K8s + - AWS + - EC2 + - VM + - CLI + - API + - IDE + - UI + - VS Code + - JetBrains + - Docker + - Terraform + - kubectl + - Helm + - GitHub + - GitLab + - OAuth + - OIDC + - SSH + - Git + - npm + - Node.js + - dev container + - dev containers + - dev environment \ No newline at end of file diff --git a/.github/docs/vale/styles/Coder/SentenceLength.yml b/.github/docs/vale/styles/Coder/SentenceLength.yml new file mode 100644 index 0000000000000..a8901782e7fa1 --- /dev/null +++ b/.github/docs/vale/styles/Coder/SentenceLength.yml @@ -0,0 +1,18 @@ +--- +# Checks for sentences that are too long but allows a more conversational style +extends: metric +message: "Consider splitting this sentence or simplifying it - it's %s characters long" +link: https://developers.google.com/style/sentence-structure +level: suggestion +scope: sentence +metrics: + - type: character + min: 10 + max: 200 # More generous limit than standard guides + +# Exemptions for specific types of content that may have longer sentences +exceptions: + - code blocks + - command explanations + - configuration examples + - URLs \ No newline at end of file diff --git a/.github/docs/vale/styles/Coder/Terms.yml b/.github/docs/vale/styles/Coder/Terms.yml new file mode 100644 index 0000000000000..cbe66b323356a --- /dev/null +++ b/.github/docs/vale/styles/Coder/Terms.yml @@ -0,0 +1,48 @@ +--- +# Coder project-specific terminology and preferred terms +extends: substitution +message: "Use '%s' instead of '%s'." +level: warning +ignorecase: true +swap: + # Capitalization and product names - relaxed for documentation style + # Allow both forms of these terms - depends on context + # 'vm': 'virtual machine' + # 'VM': 'virtual machine' + # Allow K8s as shorthand for Kubernetes + # 'k8s': 'Kubernetes' + # 'K8s': 'Kubernetes' + 'kubernetes': 'Kubernetes' + # Allow both forms - AWS EC2 and Amazon EC2 are both acceptable + # 'aws ec2': 'Amazon EC2' + # 'AWS EC2': 'Amazon EC2' + 'terraform': 'Terraform' + 'docker': 'Docker' + 'github': 'GitHub' + 'oauth': 'OAuth' + 'oidc': 'OIDC' + + # UI and documentation terms + 'CLI tool': 'CLI' + 'web UI': 'dashboard' + 'web ui': 'dashboard' + 'WebUI': 'dashboard' + 'UI interface': 'user interface' + 'user-interface': 'user interface' + + # Technical terminology - allow informal usage + 'workspace instance': 'workspace' + # Allow 'dev environment' as informal shorthand + # 'dev environment': 'development environment' + # 'developer environment': 'development environment' + 'cloud-instance': 'cloud instance' + # Allow 'dev container' as it's widely used in docs + # 'dev container': 'development container' + # 'dev-container': 'development container' + + # Consistency in product features + 'workspace template': 'template' + 'remote-development': 'remote development' + 'self-hosted': 'self-hosted' + 'on-prem': 'self-hosted' + 'on-premise': 'self-hosted' \ No newline at end of file diff --git a/.github/docs/vale/styles/GitLab/Spelling.yml b/.github/docs/vale/styles/GitLab/Spelling.yml new file mode 100644 index 0000000000000..ebd873f73cf65 --- /dev/null +++ b/.github/docs/vale/styles/GitLab/Spelling.yml @@ -0,0 +1,37 @@ +--- +# GitLab spelling checks aligned with their style guide +extends: spelling +message: "Did you mean '%s'?" +level: error +ignore: docs/glossary.md +swap: + # Technology terms + "access(?:ing|ed)? through the UI": to use the user interface + "cli ?commands?": command line commands + "command ?line": command-line + "e[ -]mail": email + "file ?name": filename + "java[ -]script": JavaScript + "node[ .]js": Node.js + "on[ -]premise": on-premises + "pre[ -]requisite": prerequisite + "style[ -]guide": style guide + "type[ -]script": TypeScript + "user ?name": username + + # GitLab preferred spellings + "admin[ -]level": administrator-level + "allowlist": allow list + "auto[ -]devops": Auto DevOps + "denylist": deny list + "dev ?ops": DevOps + "down[ -]time": downtime + "jira": Jira + "k8's": Kubernetes + "log[ -]in": login + "pgp key": PGP key + "run[ -]book": runbook + "sign[ -]in": sign in + "ssh key": SSH key + "two factor": two-factor + "web ?hook": webhook \ No newline at end of file diff --git a/.github/docs/vale/styles/GitLab/SubstitutionWarning.yml b/.github/docs/vale/styles/GitLab/SubstitutionWarning.yml new file mode 100644 index 0000000000000..45c87e9d4dd15 --- /dev/null +++ b/.github/docs/vale/styles/GitLab/SubstitutionWarning.yml @@ -0,0 +1,29 @@ +--- +# GitLab style guide substitutions - Warning level +extends: substitution +message: "Use '%s' instead of '%s'." +level: warning +ignorecase: true +swap: + 'back-end': 'backend' + 'front-end': 'frontend' + 'web site': 'website' + 'web-site': 'website' + 'click on': 'click' + 'server side': 'server-side' + 'client side': 'client-side' + 'real-time': 'real time' + 'repo': 'repository' + 'utilize': 'use' + 'execution': 'run' + 'leverage': 'use' + 'terminate': 'stop' + 'abort': 'stop' + 'kill': 'stop' + 'implement': 'create' + 'desire': 'want' + 'robust': 'reliable' + 'dropdown': 'drop-down' + 'popup': 'pop-up' + 'in order to': 'to' + 'lets': 'let''s' \ No newline at end of file diff --git a/.github/workflows/docs-preview.yaml b/.github/workflows/docs-preview.yaml new file mode 100644 index 0000000000000..f4a9319280c13 --- /dev/null +++ b/.github/workflows/docs-preview.yaml @@ -0,0 +1,107 @@ +name: Docs Preview +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'docs/**' + +permissions: + contents: read + +jobs: + preview: + name: Generate docs preview + runs-on: ubuntu-latest + permissions: + pull-requests: write # needed for commenting on PRs + steps: + - name: Harden Runner + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Get PR info + id: pr_info + run: | + set -euo pipefail + PR_NUMBER=${{ github.event.pull_request.number }} + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99 # v45.0.7 + with: + files: | + docs/** + + - name: Debug changed files + run: | + echo "All changed files: ${{ steps.changed-files.outputs.all_changed_files }}" + echo "JSON format: ${{ steps.changed-files.outputs.all_changed_files_json }}" + + - name: Check if manifest changed + id: manifest-check + run: | + echo "changed=${{ contains(steps.changed-files.outputs.all_changed_files, 'docs/manifest.json') }}" >> $GITHUB_OUTPUT + + - name: Generate docs preview + id: docs-preview + uses: ./.github/docs/actions/docs-preview + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + changed-files: ${{ steps.changed-files.outputs.all_changed_files_json }} + manifest-changed: ${{ steps.manifest-check.outputs.changed }} + + - name: Debug outputs + run: | + echo "Has changes: ${{ steps.docs-preview.outputs.has_changes }}" + echo "URL: ${{ steps.docs-preview.outputs.url }}" + echo "Changed files:" + echo "${{ steps.docs-preview.outputs.changed_files }}" + + - name: Find existing comment + if: steps.docs-preview.outputs.has_changes == 'true' + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 + id: find-comment + with: + issue-number: ${{ env.PR_NUMBER }} + comment-author: 'github-actions[bot]' + body-includes: '## 📚 Docs Preview' + direction: last + + - name: Create or update preview comment + if: steps.docs-preview.outputs.has_changes == 'true' + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ env.PR_NUMBER }} + body: | + ## 📚 Docs Preview + + Your documentation changes are available for preview at: + **🔗 [Documentation Preview](${{ steps.docs-preview.outputs.url }})** + + ### Changed Documentation Files + ${{ steps.docs-preview.outputs.changed_files }} + + ${{ steps.docs-preview.outputs.has_new_docs == 'true' && '### Newly Added Documentation' || '' }} + ${{ steps.docs-preview.outputs.has_new_docs == 'true' && steps.docs-preview.outputs.new_docs || '' }} + + ${{ steps.docs-preview.outputs.has_new_docs == 'true' && '### Preview Links for New Docs' || '' }} + ${{ steps.docs-preview.outputs.has_new_docs == 'true' && steps.docs-preview.outputs.preview_links || '' }} + + --- + 🤖 This comment is automatically generated and updated when documentation changes. + edit-mode: replace + reactions: eyes + reactions-edit-mode: replace \ No newline at end of file diff --git a/.github/workflows/docs-shared-example.yaml b/.github/workflows/docs-shared-example.yaml new file mode 100644 index 0000000000000..ac538c2ba8b06 --- /dev/null +++ b/.github/workflows/docs-shared-example.yaml @@ -0,0 +1,72 @@ +name: Docs Shared Example +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'docs/**' + - '**.md' + - '.github/workflows/docs-shared-example.yaml' + - '.github/docs/actions/docs-shared/**' + +permissions: + contents: read + +jobs: + docs-check: + name: Check Documentation + runs-on: ubuntu-latest + permissions: + pull-requests: write # needed for commenting on PRs + steps: + - name: Harden Runner + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Get PR info + id: pr_info + run: | + set -euo pipefail + PR_NUMBER=${{ github.event.pull_request.number }} + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Process Documentation + id: docs-shared + uses: ./.github/docs/actions/docs-shared + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docs-dir: docs + include-md-files: "true" + check-links: "true" + lint-markdown: "true" + format-markdown: "true" + generate-preview: "true" + post-comment: "true" + pr-number: "${{ env.PR_NUMBER }}" + fail-on-error: "false" # Set to false for this example to show all checks + + - name: Debug Outputs + run: | + echo "Has changes: ${{ steps.docs-shared.outputs.has_changes }}" + echo "Preview URL: ${{ steps.docs-shared.outputs.preview_url }}" + echo "Manifest changed: ${{ steps.docs-shared.outputs.manifest_changed }}" + echo "New docs found: ${{ steps.docs-shared.outputs.has_new_docs }}" + + # Only display errors if there are any + if [ "${{ steps.docs-shared.outputs.lint_results }}" != "" ]; then + echo "Linting issues found:" + echo "${{ steps.docs-shared.outputs.lint_results }}" + fi + + if [ "${{ steps.docs-shared.outputs.format_results }}" != "" ]; then + echo "Formatting issues found:" + echo "${{ steps.docs-shared.outputs.format_results }}" + fi \ No newline at end of file diff --git a/.github/workflows/docs-unified.yaml b/.github/workflows/docs-unified.yaml new file mode 100644 index 0000000000000..fb39ed94c5411 --- /dev/null +++ b/.github/workflows/docs-unified.yaml @@ -0,0 +1,75 @@ +name: Docs Unified Checks +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'docs/**' + - '**.md' + - '.github/docs/**' + - '.github/workflows/docs-unified.yaml' + +permissions: + contents: read + +jobs: + docs-check: + name: Documentation Validation + runs-on: ubuntu-latest + permissions: + pull-requests: write # needed for commenting on PRs + steps: + - name: Harden Runner + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Get PR info + id: pr_info + run: | + set -euo pipefail + PR_NUMBER=${{ github.event.pull_request.number }} + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Process Documentation + id: docs-shared + uses: ./.github/docs/actions/docs-shared + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docs-dir: docs + include-md-files: "true" + check-links: "true" + lint-markdown: "true" + format-markdown: "true" + lint-vale: "true" + generate-preview: "true" + post-comment: "true" + pr-number: "${{ env.PR_NUMBER }}" + fail-on-error: "false" # Set to false to show all issues in one run + + - name: Debug Outputs + run: | + echo "Has changes: ${{ steps.docs-shared.outputs.has_changes }}" + echo "Preview URL: ${{ steps.docs-shared.outputs.preview_url }}" + echo "Manifest changed: ${{ steps.docs-shared.outputs.manifest_changed }}" + echo "New docs found: ${{ steps.docs-shared.outputs.has_new_docs }}" + + # Only display errors if there are any + if [ "${{ steps.docs-shared.outputs.lint_results }}" != "" ]; then + echo "Linting issues found" + fi + + if [ "${{ steps.docs-shared.outputs.format_results }}" != "" ]; then + echo "Formatting issues found" + fi + + if [ "${{ steps.docs-shared.outputs.vale_results }}" != "" ]; then + echo "Vale style issues found" + fi \ No newline at end of file diff --git a/.github/workflows/test-docs-shared.yaml b/.github/workflows/test-docs-shared.yaml new file mode 100644 index 0000000000000..ac538c2ba8b06 --- /dev/null +++ b/.github/workflows/test-docs-shared.yaml @@ -0,0 +1,72 @@ +name: Docs Shared Example +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'docs/**' + - '**.md' + - '.github/workflows/docs-shared-example.yaml' + - '.github/docs/actions/docs-shared/**' + +permissions: + contents: read + +jobs: + docs-check: + name: Check Documentation + runs-on: ubuntu-latest + permissions: + pull-requests: write # needed for commenting on PRs + steps: + - name: Harden Runner + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Get PR info + id: pr_info + run: | + set -euo pipefail + PR_NUMBER=${{ github.event.pull_request.number }} + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Process Documentation + id: docs-shared + uses: ./.github/docs/actions/docs-shared + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docs-dir: docs + include-md-files: "true" + check-links: "true" + lint-markdown: "true" + format-markdown: "true" + generate-preview: "true" + post-comment: "true" + pr-number: "${{ env.PR_NUMBER }}" + fail-on-error: "false" # Set to false for this example to show all checks + + - name: Debug Outputs + run: | + echo "Has changes: ${{ steps.docs-shared.outputs.has_changes }}" + echo "Preview URL: ${{ steps.docs-shared.outputs.preview_url }}" + echo "Manifest changed: ${{ steps.docs-shared.outputs.manifest_changed }}" + echo "New docs found: ${{ steps.docs-shared.outputs.has_new_docs }}" + + # Only display errors if there are any + if [ "${{ steps.docs-shared.outputs.lint_results }}" != "" ]; then + echo "Linting issues found:" + echo "${{ steps.docs-shared.outputs.lint_results }}" + fi + + if [ "${{ steps.docs-shared.outputs.format_results }}" != "" ]; then + echo "Formatting issues found:" + echo "${{ steps.docs-shared.outputs.format_results }}" + fi \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index b5a07021d3670..ef2134079e9b0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -144,3 +144,4 @@ or [the v2 migration guide and FAQ](https://coder.com/docs/v1/guides/v2-faq). - [Template](./admin/templates/index.md) - [Installing Coder](./install/index.md) - [Quickstart](./tutorials/quickstart.md) to try Coder out for yourself. +# Test heading