diff --git a/.buildkite/ecr-scan-results-ignore.yml b/.buildkite/ecr-scan-results-ignore.yml index 4293ef3e1a6..43b5acbd26e 100644 --- a/.buildkite/ecr-scan-results-ignore.yml +++ b/.buildkite/ecr-scan-results-ignore.yml @@ -18,4 +18,8 @@ ignores: - id: CVE-2024-0567 # gnutls28 3.7.9-2+deb12u1 - id: CVE-2023-50387 # systemd 252.17-1~deb12u1 - id: CVE-2024-0553 # gnutls28 3.7.9-2 - - id: CVE-2024-0567 # gnutls28 3.7.9-2+deb12u1 \ No newline at end of file + - id: CVE-2024-0567 # gnutls28 3.7.9-2+deb12u1 + - id: CVE-2024-37371 # krb5 1.20.1-2+deb12u1 + - id: CVE-2024-37370 # krb5 1.20.1-2+deb12u1 + - id: CVE-2024-10963 # pam 1.5.2-6+deb12u1 + - id: CVE-2025-32990 # gnutls28 3.7.9-2+deb12u3 diff --git a/.buildkite/pipeline.deploy.yml b/.buildkite/pipeline.deploy.yml index 44ff7d9637a..158d1fc3f3c 100644 --- a/.buildkite/pipeline.deploy.yml +++ b/.buildkite/pipeline.deploy.yml @@ -1,3 +1,6 @@ +env: + BUILDKIT_PROGRESS: plain + steps: - name: ":docker::ecr: Push to ECR" command: ".buildkite/push-image.sh" @@ -6,23 +9,35 @@ steps: if: | build.branch == "main" agents: - queue: elastic-runners + queue: hosted plugins: - - aws-assume-role-with-web-identity#v1.0.0: - role-arn: arn:aws:iam::${ECR_ACCOUNT_ID}:role/pipeline-buildkite-docs-main + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::${ECR_ACCOUNT_ID}:role/pipeline-buildkite-docs-private + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - build_branch - ecr#v2.7.0: login: true account-ids: ${ECR_ACCOUNT_ID} - name: ":ecr: ECR Vulnerabilities Scan" command: "true" + soft_fail: + - exit_status: 75 # soft fail for scan timeouts agents: - queue: elastic-runners + queue: hosted depends_on: "ecr-push" plugins: - - aws-assume-role-with-web-identity#v1.0.0: - role-arn: arn:aws:iam::${ECR_ACCOUNT_ID}:role/pipeline-buildkite-docs-main - - buildkite/ecr-scan-results#v2.0.0: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::${ECR_ACCOUNT_ID}:role/pipeline-buildkite-docs-private + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - build_branch + - cultureamp/ecr-scan-results#v1.7.0: image-name: "${ECR_REPO}:${BUILDKITE_BUILD_NUMBER}" fail-build-on-plugin-failure: true @@ -48,11 +63,16 @@ steps: concurrency: 1 concurrency_group: docs-deploy agents: - queue: elastic-runners + queue: hosted command: scripts/deploy-ecs plugins: - - aws-assume-role-with-web-identity#v1.0.0: - role-arn: arn:aws:iam::${ECR_ACCOUNT_ID}:role/pipeline-buildkite-docs-main + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::${ECR_ACCOUNT_ID}:role/pipeline-buildkite-docs-private + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - build_branch - wait diff --git a/.buildkite/pipeline.graphql.yml b/.buildkite/pipeline.graphql.yml index d20355fff88..c773491df0a 100644 --- a/.buildkite/pipeline.graphql.yml +++ b/.buildkite/pipeline.graphql.yml @@ -1,3 +1,14 @@ steps: - label: Update GraphQL docs command: .buildkite/update_graphql_docs + plugins: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs-private + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - aws-ssm#v1.0.0: + parameters: + GH_TOKEN: /pipelines/buildkite/docs-private/gh-token + API_ACCESS_TOKEN: /pipelines/buildkite/docs-private/api-access-token diff --git a/.buildkite/pipeline.preview.yml b/.buildkite/pipeline.preview.yml index 5c17bae6a33..46483c56deb 100644 --- a/.buildkite/pipeline.preview.yml +++ b/.buildkite/pipeline.preview.yml @@ -1,18 +1,50 @@ +env: + BUILDKIT_PROGRESS: plain + steps: - label: "Prepare preview" command: bin/prepare-preview + key: prepare-preview + plugins: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs-private + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - aws-ssm#v1.0.0: + parameters: + GH_TOKEN: /pipelines/buildkite/docs-private/gh-token + BUILDKITE_ANALYTICS_TOKEN: /pipelines/buildkite/docs-private/buildkite-analytics-token + NETLIFY_AUTH_TOKEN: /pipelines/buildkite/docs-private/netlify-auth-token + NETLIFY_SITE_ID: /pipelines/buildkite/docs-private/netlify-site-id - label: "Deploy preview" command: bin/deploy-preview + depends_on: prepare-preview env: RAILS_ENV: "production" plugins: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs-private + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - aws-ssm#v1.0.0: + parameters: + GH_TOKEN: /pipelines/buildkite/docs-private/gh-token + BUILDKITE_ANALYTICS_TOKEN: /pipelines/buildkite/docs-private/buildkite-analytics-token + NETLIFY_AUTH_TOKEN: /pipelines/buildkite/docs-private/netlify-auth-token + NETLIFY_SITE_ID: /pipelines/buildkite/docs-private/netlify-site-id - docker-compose#v3.9.0: - run: app + run: deploy-preview dependencies: false mount-buildkite-agent: true env: + - GH_REPO - GH_TOKEN - NETLIFY_AUTH_TOKEN - NETLIFY_SITE_ID + - NETLIFY_SALT - BUILDKITE_BRANCH diff --git a/.buildkite/pipeline.sync.yml b/.buildkite/pipeline.sync.yml new file mode 100644 index 00000000000..d1329b9b226 --- /dev/null +++ b/.buildkite/pipeline.sync.yml @@ -0,0 +1,22 @@ +#docs-private-to-public-sync +notify: + - slack: "$SLACK_CHANNEL" + if: build.state != "passed" + +steps: + - label: Sync to public mirror + branches: main + commands: | + echo "+++ :git: Syncing to public mirror" + + git push "https://dummy-user:$${GH_TOKEN}@github.com/buildkite/docs.git" ${BUILDKITE_COMMIT}:main + plugins: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs-private + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - aws-ssm#v1.0.0: + parameters: + GH_TOKEN: /pipelines/buildkite/docs-private/gh-token diff --git a/.buildkite/pipeline.sync_public_pr.yml b/.buildkite/pipeline.sync_public_pr.yml new file mode 100644 index 00000000000..f54720252e5 --- /dev/null +++ b/.buildkite/pipeline.sync_public_pr.yml @@ -0,0 +1,30 @@ +steps: + # NOTE: this input step is in the pipeline steps on Buildkite + # - block: "Which PR needs to be synced?" + # fields: + # - key: pull_request_number + # required: true + # text: "Pull Request Number" + # format: "^[0-9]+$" + # hint: "Find the PR number on https://github.com/buildkite/docs/pulls" + + - label: ":git: Sync Public PR to Private Repo" + command: "bin/sync-public-pr" + plugins: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs-private + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - aws-ssm#v1.0.0: + parameters: + GH_TOKEN: /pipelines/buildkite/docs-private/gh-token + - docker#v5.11.0: + image: "ruby:3.3-bookworm" + propagate-environment: true + mount-buildkite-agent: true + environment: + - GH_TOKEN + - GH_PRIVATE_REPO + - GH_PUBLIC_REPO diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 192e188bead..ec201c4103b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,45 +1,86 @@ +env: + BUILDKIT_PROGRESS: plain + steps: - - label: ":spider: muffet" - # We need to wait until rails has started before running muffet as otherwise it will error out - # and the test will appear to have failed without having run. The time to wait is hard to - # predict, and furthermore, some paths take longer to be be ready than others. The path in this - # loop was chosen after some non-systematic observations. So it does not guarantee that the - # server will be ready. But it seems to work well in practice. - command: >- - while ! wget --spider -Sq http://app:3000/docs/agent/v3/hooks; - do echo šŸ’ŽšŸ›¤ļøšŸ¦„ Rails is still starting; - sleep 0.5; - done && - echo šŸ’ŽšŸ›¤ļøšŸš† Rails has started running && - /muffet http://app:3000/docs \ - --include="/docs/" \ - --exclude="https://github.com/buildkite/docs/" \ - --exclude="buildkite.com/docs" \ - --max-connections=10 \ - --color=always + - label: ":spider: Check for link issues" env: RAILS_ENV: production agents: queue: hosted + soft_fail: + - exit_status: 1 plugins: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - ecr#v2.9.0: + login: true + account-ids: public.ecr.aws - docker-compose#v3.9.0: run: muffet shell: false upload-container-logs: always - env: - - RAILS_ENV + mount-buildkite-agent: true + propagate-environment: true + artifact_paths: + - ".buildkite/steps/muffet_results.json" - label: ":graphql: Check generated code is up to date" command: bundle exec rake graphql:generate agents: queue: hosted plugins: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - ecr#v2.9.0: + login: true + account-ids: public.ecr.aws - docker-compose#v3.9.0: run: app dependencies: false - .buildkite/plugins/check-for-changes: fail_message: "Generated code is out of date. Run `bundle exec rake graphql:generate` and commit the changes." + - label: ":go: Check generated agent CLI docs" + if_changed: pages/agent/v3/cli/help/*.md + command: ./scripts/update-agent-help.sh + agents: + queue: hosted-m + plugins: + - docker#v5.13.0: + image: "golang:1.24" + - .buildkite/plugins/check-for-changes: + fail_message: "Do not edit agent CLI docs directly - use scripts/update-agent-help.sh" + + - label: ":go: Check generated BK CLI docs" + if_changed: pages/platform/cli/*.md + command: ./scripts/update-bkcli-help.sh + agents: + queue: hosted-m + plugins: + - docker#v5.13.0: + image: "golang:1.24" + - .buildkite/plugins/check-for-changes: + fail_message: "Do not edit BK CLI docs directly - use scripts/update-bkcli-help.sh" + + - label: ":test_tube: Check generated agent experiments docs" + if_changed: pages/agent/v3/self_hosted/configure/experiments.md + command: ./scripts/update-agent-experiments.sh + agents: + queue: hosted-m + plugins: + - docker#v5.13.0: + image: "ruby:3.3" + - .buildkite/plugins/check-for-changes: + fail_message: "Do not edit experiments.md directly - update https://github.com/buildkite/agent/blob/main/EXPERIMENTS.md and use scripts/update-agent-experiments.sh" + - group: "Lint all the things" key: linting steps: @@ -50,13 +91,17 @@ steps: agents: queue: hosted plugins: - - aws-assume-role-with-web-identity#v1.0.0: + - aws-assume-role-with-web-identity#v1.4.0: role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs + session-tags: + - organization_slug + - organization_id + - pipeline_slug - ecr#v2.9.0: login: true account-ids: public.ecr.aws - docker#v3.7.0: - image: "public.ecr.aws/docker/library/node:lts-alpine3.14" + image: "public.ecr.aws/docker/library/node:lts-alpine3.16" - label: ":lint-roller: Linting text and markdown" agents: @@ -71,8 +116,12 @@ steps: agents: queue: hosted plugins: - - aws-assume-role-with-web-identity#v1.0.0: + - aws-assume-role-with-web-identity#v1.4.0: role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs + session-tags: + - organization_slug + - organization_id + - pipeline_slug - ecr#v2.9.0: login: true account-ids: public.ecr.aws @@ -80,9 +129,7 @@ steps: # version, is way faster because we don't have to wait for Ruby gems to # install. - docker#v3.7.0: - # Alpine 3.14 - EOS 01 May 2023 - # lts = Node 1415.1 - 2023-04-30 - image: "public.ecr.aws/docker/library/node:lts-alpine3.14" + image: "public.ecr.aws/docker/library/node:lts-alpine3.16" - label: "Validate YAML" command: @@ -91,39 +138,51 @@ steps: agents: queue: hosted plugins: - - aws-assume-role-with-web-identity#v1.0.0: + - aws-assume-role-with-web-identity#v1.4.0: role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs + session-tags: + - organization_slug + - organization_id + - pipeline_slug - ecr#v2.9.0: login: true account-ids: public.ecr.aws - docker#v3.7.0: - image: "public.ecr.aws/docker/library/node:lts-alpine3.14" + image: "public.ecr.aws/docker/library/node:lts-alpine3.16" - label: ":lint-roller: :markdown: Linting the Markdown" command: npm run -y mdlint agents: queue: hosted plugins: - - aws-assume-role-with-web-identity#v1.0.0: + - aws-assume-role-with-web-identity#v1.4.0: role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs + session-tags: + - organization_slug + - organization_id + - pipeline_slug - ecr#v2.9.0: login: true account-ids: public.ecr.aws - docker#v3.7.0: - image: "public.ecr.aws/docker/library/node:lts-alpine3.14" + image: "public.ecr.aws/docker/library/node:lts-alpine3.16" - label: ":snake: Linting markdown files for snake case" command: npx -y @ls-lint/ls-lint@2.1.0 agents: queue: hosted plugins: - - aws-assume-role-with-web-identity#v1.0.0: + - aws-assume-role-with-web-identity#v1.4.0: role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs + session-tags: + - organization_slug + - organization_id + - pipeline_slug - ecr#v2.9.0: login: true account-ids: public.ecr.aws - docker#v3.7.0: - image: "public.ecr.aws/docker/library/node:lts-alpine3.14" + image: "public.ecr.aws/docker/library/node:lts-alpine3.16" - label: ":rspec: RSpec" depends_on: linting @@ -135,6 +194,15 @@ steps: agents: queue: hosted plugins: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - ecr#v2.9.0: + login: true + account-ids: public.ecr.aws - docker-compose#v3.9.0: run: app dependencies: false @@ -151,13 +219,21 @@ steps: - RAILS_ENV - label: ":docker: Build image" - depends_on: "linting" key: "assets" env: RAILS_ENV: production agents: queue: hosted plugins: + - aws-assume-role-with-web-identity#v1.4.0: + role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-docs + session-tags: + - organization_slug + - organization_id + - pipeline_slug + - ecr#v2.9.0: + login: true + account-ids: public.ecr.aws - docker-compose#v3.9.0: build: app diff --git a/.buildkite/push-image.sh b/.buildkite/push-image.sh index e54fabbd5cc..39a2588aea7 100755 --- a/.buildkite/push-image.sh +++ b/.buildkite/push-image.sh @@ -7,6 +7,7 @@ echo "--- :docker: Building docker image" TAG="${BUILDKITE_BUILD_NUMBER}" docker build -t "$ECR_REPO:$TAG" \ + --target="runtime" \ --build-arg="RAILS_ENV=production" \ --build-arg="DD_RUM_VERSION=$BUILDKITE_BUILD_NUMBER" \ --build-arg="DD_RUM_ENV=production" \ diff --git a/.buildkite/steps/link-checking-rules.yaml b/.buildkite/steps/link-checking-rules.yaml new file mode 100644 index 00000000000..5dfeef9ba42 --- /dev/null +++ b/.buildkite/steps/link-checking-rules.yaml @@ -0,0 +1,85 @@ +link_checking_exemptions: + - name: ignore-all-errors + description: | + Ignore all errors. + All failures are ignored for these prefixes. + status_pattern: ".*" + url_patterns: + - https://github.com/buildkite/docs/ + - https://buildkite.com/my-organization/ + - https://github.com/my-org/ + - https://claude.ai/download + + - name: ignore-page-changes-in-pr + description: | + Ignores HTTP 404 errors generated from new pages in PR, + as well as pages whose name or path, or both, has changed in this PR. + status_pattern: "404" + url_patterns: + - https://buildkite.com/docs/ + + - name: login-required + description: | + Ignore HTTP 403 Forbidden. + URLs matching these regular expressions are known not to work unless you are logged in. + status_pattern: "403" + url_patterns: + - https://buildkite.com/organizations/ + - https://buildkite.com/user/ + - https://buildkite.com/(~|%7E)/ + - https://portal.azure.com/ + + - name: accept-anti-scraping-measures + description: Muffet resembles a scraper to some sites + status_pattern: "(400|403)" + url_patterns: + - https://www.npmjs.com/ + - https://linux.die.net/man/ + - https://www.seek.com.au/ + - https://www.cvedetails.com/ + - https://github.com/marketplace + - https://codepen.io/ + - https://www.java.com + + - name: ignore-api-401-unauthorized + description: Ignore HTTP 401 Unauthorized on Buildkite API + status_pattern: "401" + url_patterns: + - https://api.buildkite.com/ + + - name: accept-429-too-many-requests + description: Ignore rate limiting. + status_pattern: "429" + url_patterns: + # Everything + - ".*" + + - name: fragments-handled-by-js-not-html + description: Sites that interpret URL fragments using JS, rather than standard browser behavior + status_pattern: "id #.* not found" + url_patterns: + - https://github.com/ + - https://cd.apps.argoproj.io/swagger-ui + - https://docs.cursor.com/ + - https://developer.hashicorp.com/terraform/registry/modules/publish + - https://docs.windsurf.com/windsurf/cascade/mcp + + - name: surprising-fragment-failures + description: Surprisingly, these links fail because of missing anchors, not authentication. + status_pattern: "id #.* not found" + url_patterns: + - https://console.cloud.google.com/compute/instancesAdd + - https://console.aws.amazon.com/cloudformation/home + - https://console.aws.amazon.com/ec2/v2/home + + - name: ignore-sample-dot-svg + description: We don't expect /sample.svg to work on the test server that Muffet uses. + status_pattern: "404" + url_patterns: + - "http://app:3000/sample.svg\\?" + + - name: not-actually-links + description: Muffet can be a bit greedy sometimes, and pick up URLs that are not actually links, but (for example) part of code samples. + status_pattern: ".*" + url_patterns: + - https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name diff --git a/.buildkite/steps/muffet.rb b/.buildkite/steps/muffet.rb new file mode 100755 index 00000000000..3651539b1c5 --- /dev/null +++ b/.buildkite/steps/muffet.rb @@ -0,0 +1,185 @@ +#!/usr/bin/env ruby + +require 'yaml' +require 'json' + +require 'open3' + +def annotate!(annotation:, context:, style: "info") + annotated = false + if ENV["BUILDKITE"] == 'true' + puts "Uploading annotation (#{annotation.size} bytes)" + Open3.popen3(%Q[buildkite-agent annotate --style "#{style}" --context "#{context}"]) do |stdin, _, _, wait_thr| + stdin.puts(annotation) + stdin.close + + annotated = (wait_thr.value.exitstatus == 0) + end + end + + unless annotated + puts "--- ANNOTATION [#{style},#{context}]" + puts annotation + puts + end +end + +def rules + return @rules if @rules + + rules_yaml = YAML.load(File.read('link-checking-rules.yaml')) + + @rules = rules_yaml['link_checking_exemptions'].each_with_object([]) do |r, arr| + arr << { + name: r['name'], + description: r['description'], + status_pattern: /^#{r['status_pattern']}$/, + url_patterns: r['url_patterns'].map {|url| /^#{url}/ } + } + end +end + +@failed = {} +def result_fail(page, link) + @failed[page] ||= [] + + @failed[page] << link +end + +@passed = {} +def result_pass(page, link, decided_by) + @passed[page] ||= [] + + @passed[page] << link.merge({'decided_by' => decided_by}) +end + +puts "--- Waiting for app to start" + +until system('wget --spider -S http://app:3000/docs/agent/v3/self-hosted/hooks') + puts "šŸ’ŽšŸ›¤ļøšŸ¦„ Rails is still starting" + sleep 0.5 +end +puts "šŸ’ŽšŸ›¤ļøšŸš† Rails has started running" + +puts "--- Running muffet" + +muffet_cmd = [ + '/muffet', + 'http://app:3000/docs', + '--header="User-Agent: Muffet/$(/muffet --version)"', + '--max-connections=10', + '--timeout=15', + '--buffer-size=8192', + '--format=json', + # Capture successes as well as failures + '--verbose', + ].join(' ') + +muffet_output_json=`#{muffet_cmd}` + +File.write('muffet_results.json', muffet_output_json) + +puts "--- Checking results" + + +pages = JSON.load(muffet_output_json) + +pages.each do |page| + page['links'].each do |link| + unless link.has_key?('error') + next + end + + # There is an error. Do we have an exempting rule for it? + exemptors = rules.select do |rule| + rule[:status_pattern].match?(link['error']) && + rule[:url_patterns].any? {|patt| patt.match?(link['url']) } + end + + if exemptors.any? + result_pass(page['url'], link, exemptors) + else + result_fail(page['url'], link) + end + + end +end + +report = "" +if @failed.any? + report = <<~MARKDOWN + ## Muffet found potentially broken links + + Resolve _genuine broken links_ with either a **404** or **id #fragment not found** status first. + + Ignore links with a **timeout** / **timed out** status (since these are usually only temporarily broken), as well as _working links_ that return a **403** or **id #fragment not found** status, or an unusual status with a lengthy description. + + Configure any _consistently working_ **403** or **id #fragment not found** status links as exceptions in the relevant section of the `link-checking-rules.yaml` file, which moves them to the **Non-breaking failures** section below. + + MARKDOWN + + @failed.each do |page, links| + path_and_query = page.sub(/https?:\/\/[^\/]+/,'') + + rows = links.reduce("") do |table, l| + table += "| #{l['url']} | #{l['error']} |\n" + end + + report += <<~MARKDOWN + ### In \`#{path_and_query}\`: + + | Link | Status | + |------|--------| + #{rows} + + MARKDOWN + + end + +else + report = "## Muffet found no problems :sunglasses:\n\n" +end + +if @passed.any? + report += <<~MARKDOWN + + ## Non-breaking failures + + The following requests would have failed, but we made them exempt in `.buildkite/steps/link-checking-rules.yaml`. + +
Exempt links + + MARKDOWN + + @passed.each do |page, links| + path_and_query = page.sub(/https?:\/\/[^\/]+/,'') + + rows = links.reduce("") do |table, l| + table += "| #{l['url']} | #{l['error']} | #{l['decided_by'].map {|r| r[:name] }.join(', ')} |\n" + end + + report += <<~MARKDOWN + ### In \`#{path_and_query}\`: + + | Link | Status | Deciding rule(s) | + |------|--------|------------------| + #{rows} + + MARKDOWN + end + + report += "
\n\n" +end + +report += "The complete results (including **all** successful requests) will be uploaded in JSON format as a build artifact. If you need to figure out why links are passing checks when they shouldn't be, that is a good place to start.\n\n" + +annotate!(annotation: report, context: 'muffet') + +puts report.size +puts "Report #{report.size < 1024**2 ? 'will' : 'will not'} fit in an annotation." + +if @failed.any? + exit(1) +else + exit(0) +end diff --git a/.buildkite/update_graphql_docs b/.buildkite/update_graphql_docs index 79bc49afc68..0f695a814f2 100755 --- a/.buildkite/update_graphql_docs +++ b/.buildkite/update_graphql_docs @@ -38,8 +38,10 @@ git config user.email $GIT_EMAIL git config user.name $GIT_NAME git checkout -b $BRANCH git commit -m "Update GraphQL docs" -git push -u origin $BRANCH + +TARGET_REPO="https://dummy-user:${GH_TOKEN}@github.com/${GH_REPO}" +git push -f "${TARGET_REPO}" HEAD:${BRANCH} echo "+++ Create pull request" -create_pull_request "Update GraphQL docs" "This is an automated PR based on the current GraphQL schema" \ +create_pull_request "Update GraphQL docs" "This is an automated PR based on the current GraphQL schema" "${BRANCH}" \ | jq ".html_url" | buildkite-agent annotate --style "success" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..0881c12233e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @buildkite/docs diff --git a/.github/labeler.yml b/.github/labeler.yml index 3d70159e2eb..0e34d6298f8 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -2,8 +2,8 @@ agent: - pages/agent/**/* cli: - pages/cli/**/* -test-analytics: - - pages/test_analytics/**/* +test-engine: + - pages/test_engine/**/* pipelines: - pages/pipelines/**/* - pages/tutorials/**/* diff --git a/.gitignore b/.gitignore index 84584e408e5..053faa9eac1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ node_modules # Local Netlify folder .netlify + +# Muffet link check results +.buildkite/steps/muffet_results.json \ No newline at end of file diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 8bfdb441cf3..c93ab49951c 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -1,6 +1,6 @@ default: false -# Need to figure out how to make exceptions for pages/agent/v3/help/_* +# Need to figure out how to make exceptions for pages/agent/v3/cli/help/_* #MD002: # Heading level #level: 1 diff --git a/.nvmrc b/.nvmrc index 8b3ed1b235e..2bd5a0a98a3 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.19.1 +22 diff --git a/.ruby-version b/.ruby-version index 619b5376684..4f5e69734c9 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.3 +3.4.5 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000000..27a8619d7e4 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 3.4.5 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..51303255c24 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,586 @@ +# Buildkite documentation repository and pipeline + +This repository generates the Buildkite documentation website: + +https://buildkite.com/docs + +- Repository: `https://github.com/buildkite/docs-private` +- CI: `https://buildkite.com/buildkite/docs-private` +- CI steps: `.buildkite/pipeline.yml` + +There are GitHub Actions workflows but they are not part of the CI pipeline. **Do not use Github Actions. EVER.** When asked to do anything with CI use Buildkite. You should have the Buildkite MCP server available. If you don't, and you need CI, STOP and ask the user to set it up: + +https://github.com/buildkite/buildkite-mcp-server + +Run the CI steps locally and correct any errors before pushing commits. Review the CI build after push. + +--- + +# Buildkite documentation style rules + +## Instructions + +You are an expert technical style reviewer for Buildkite documentation. Check the document that will be provided or pointed out to you based on the writing rules outlined below. Your job is to apply the style guide rules below with 100% accuracy and no omissions or additions. Apply ALL the following rules without exception. Do not infer rules, make assumptions, or rely on general language models’ style preferences. Only follow what is explicitly stated. + +For each paragraph in the provided document, evaluate every rule and list violations. Do NOT ignore any of the rules. Do NOT make up rules not listed here. Go over the document two times. + +**Always:** +- Use only the terminology and formatting defined below +- Flag each instance of rule violation, referencing the rule being broken +- Do not suggest changes that are not covered by the rules +- Do not hallucinate missing or implied rules +- Check for typos + +**Review the content twice:** +- First pass: Check for violations against the core rules +- Second pass: Confirm consistency and identify overlooked errors, spelling errors, typos, trailing spaces + +Be strict. Do not allow edge cases to slide. + +If a rule conflicts with another (e.g., clarity vs formatting), prioritize: +- Clarity of user-facing documentation +- Consistency with UI and terminology +- Formatting standards + +Do NOT deviate. Do NOT add style suggestions based on general best practices. Only apply the rules outlined below. + +Here are the rules: + +## Core style and voice + +This style guide applies to Buildkite product documentation, API reference pages, step-by-step how-tos, and tutorials. + +**Language and Voice:** +- Use US English (Merriam Webster) +- Write in plain English, avoid unnecessary jargon +- Maintain a semi-formal tone - balance between professional and approachable +- Don't use "delve," "comprehensive," "embark," "leverage," "utilize," "unlock," "harness," or similar buzzwords +- Use active voice whenever possible +- Use contractions appropriately (didn't, haven't, etc.) +- Always use "they" for gender-neutral pronouns, NEVER "he" or "she" +- Don't use phrases like "it's important to note," "it's worth noting," "keep in mind" +- Don't start sentences with "Additionally," "Furthermore," "Moreover" +- Don't use redundant emphasis like "really," "very," or "quite" +- Don't be overly enthusiastic, don't use unnecessary exclamation marks + +**Formatting standards:** +- Use sentence case for ALL headings. Only capitalize the first word and proper nouns. Example: "Setting up your first pipeline" not "Setting Up Your First Pipeline". +- Format Buildkite UI elements in **bold** matching exact Buildkite interface capitalization. ONLY use bold for Buildkite UI element names (buttons, menu items, field names, tabs, etc.). Nothing else should be bolded. Not for emphasis, not for key terms, not for anything except UI elements. +- Format key terms and emphasis in _italics_ (use sparingly) +- Use serial commas when listing items +- Don't use emojis in lists +- In paragraphs, write out numbers up to 10, then use digits; in headings - AVOID digits for numbers smaller than 10 +- Use 24-hour time format with timezone (e.g., 17:00 AEST) + +## Technical writing rules + +When writing technical documentation for Buildkite: + +**Terminology:** +- "Buildkite Agent" or "agent" when referring to the running process +- `buildkite-agent` (in code blocks) when referring to the CLI tool +- "Sign up/log in" (verbs) vs "signup/login" (nouns/adjectives) +- "Time out" (verb) vs "timeout" (noun/adjective) +- Always capitalize: API, SSO, SAML +- Use "Two-factor authentication" (short form: 2FA) +- Use "Single sign-on" (short form: SSO) + +**Structure:** +- Use bullet lists for unordered items +- Use numbered steps for sequential instructions +- Capitalize first word in lists, use periods only for complete sentences +- Avoid "and/or" - use "or" or rephrase with "or both" +- Use "and" not "&" +- Avoid "we" and "our" in formal documentation +- Avoid exclamation marks in formal content + +## Code documentation rules + +When documenting code or technical processes for Buildkite: + +**Code references:** +- Use code blocks with language identifiers (```yaml, ```bash, etc.) +- Avoid using `code` formatting in headings +- Don't grammatically inflect code elements in headings +- Present CLI commands clearly with proper formatting + +**Instructions:** +- Write step-by-step instructions using active voice +- Use "Select X > Y" format for navigation +- Be specific and actionable in instructions +- Use numbered lists for sequential processes + +## Content review checklist + +Review documentation content for: + +**Language issues:** +- Correct use of their/they're/there and your/you're +- Proper use of homonyms and words with 1 character spelling difference (seek vs. sick, though vs. through, etc.) +- Proper affect/effect usage (affect = verb, effect = noun) +- Use "for example" instead of "e.g." +- Use "that is" instead of "i.e." +- Use "and so on" instead of "etc." +- Use "using" instead of "via." +- Use "blocklist" instead of "blacklist" +- Use "allowlist" instead of "whitelist" +- Use "OAuth" instead of "oauth" +- Use "plugins directory" instead of "plugin directory" +- Appropriate hyphen usage (compound adjectives vs. verbs) + + +**Style Consistency:** +- Sentence case headings without punctuation +- Proper product name capitalization (e.g., "GitHub" not "Github") +- Consistent terminology +- Use consistent capitalization and abbreviations + +**Structure:** +- Clear, logical flow of information +- Appropriate use of lists vs. paragraphs +- Consistent formatting of similar elements +- Plain English without unnecessary complexity + +## Accessibility and clarity rules + +Ensure documentation is accessible and clear: + +**Clarity:** +- Use plain English principles +- Explain technical terms when first introduced +- Structure information logically +- Use active voice for instructions +- Keep sentences concise and direct + +**Consistency:** +- Follow established patterns for similar content +- Use consistent terminology throughout +- Maintain uniform formatting for similar elements +- Ensure headings follow a logical hierarchy + +**User focus:** +- Provide clear next steps or related information +- Use inclusive language throughout + +## Quick reference checklist + +- [ ] US English, semi-formal tone +- [ ] Active voice, plain English +- [ ] Sentence case headings, no punctuation +- [ ] Serial commas, "and" not "&" +- [ ] "They" for pronouns, numbers <10 spelled out +- [ ] Proper Buildkite terminology (Agent, buildkite-agent, etc.) +- [ ] Consistent capitalization (GitHub, API, SSO, etc.) +- [ ] Clear structure with appropriate lists +- [ ] Avoid "we/our" in formal docs whenever possible +- [ ] Avoid exclamation marks + +--- + +# Buildkite Markdown syntax rules + +## File structure and Markdown engine + +**Markdown engine:** +- Uses Redcarpet Ruby library (not CommonMark or GitHub Flavored Markdown) +- Inline HTML comments are visible in output: `` +- Block HTML comments are hidden: + ```markdown + + ``` + +## Headings rules + +**Structure:** +- Always nest headings incrementally: `#` → `##` → `###` → `####` +- Use only one `#` (H1) per page as the page title +- Maximum depth is H4 (don't go deeper than `####`) +- Insert empty lines above and below all headings + +**Formatting:** +- Use sentence case (capitalize only first word and proper nouns) +- No punctuation at end of headings +- Avoid `code` formatting in headings +- Avoid **bold** or _italic_ formatting in headings + +**Example:** +```markdown +# Page title + +## Main section + +### Subsection + +#### Detail section + +## Another main section +``` + +## Paragraph and line break rules + +**New paragraphs:** +- Use two line breaks (one empty line) between paragraphs +- Never use `
` tags for single line breaks + +**List item paragraphs:** +- Use exactly 4 spaces to indent new paragraphs within list items +- This prevents breaking out of the list structure + +**Example:** +```markdown +1. First paragraph of list item. + + Second paragraph within same list item (4 spaces). + +2. Next list item. +``` + +## Spacing rules + +**Sentence spacing:** +- Use only one space after punctuation at end of sentences +- Do not leave trailing spaces at the end of sentences or list items +- Never use double spaces + +## Formatting rules + +**Bold text (Buildkite UI elements):** +- Use `**text**` (double asterisks) for bold +- Never use `__text__` (double underscores) +- Format all Buildkite UI elements in bold + +**Italic text (key terms/emphasis):** +- Use `_text_` (single underscores) for italics +- Never use `*text*` (single asterisks) +- Use sparingly for key terms and emphasis + +## List rules + +**Unordered lists:** +- Use `-` (hyphen) for top-level items +- Use `*` (asterisk) for 2nd-level items +- Use `-` (hyphen) for 3rd-level items +- Use exactly 4-space indentation for nesting + +**Ordered lists:** +- Always use `1.` for all numbered items (don't increment manually) + +**Example:** +```markdown +1. First item on the list +1. Second item on the list +1. Third item on the list +``` + +- Use exactly 4-space indentation for nesting + +**Example:** +```markdown +- Top-level item + + * Second-level item + * Another second-level item + + - Third-level item + - Another third-level item + +- Another top-level item +``` + +## Link rules + +**Internal links:** +- Use relative URLs starting from `/docs` +- Example: `[environment variables](/docs/pipelines/environment-variables)` +- Never use absolute URLs for internal links + +**Anchor links:** +- H2 links: `/docs/page#section-name` (kebab-case) +- H3 links: `/docs/page#h2-section-h3-section` (H2 + dash + H3) + +**External links:** +- Always use full absolute URLs +- Include other Buildkite sites (main site, blog, changelog) + +## Callout rules + +**Info callouts:** +```markdown +> šŸ“˜ Callout title +> Callout content goes here. +> Each line break creates a new paragraph. +``` + +**Warning/troubleshooting callouts:** +```markdown +> 🚧 Warning title +> Warning content goes here. +``` + +**Callouts in numbered lists:** +- Use indented bold text instead of emoji format +- Example: +```markdown +1. Step one. + + **Note:** This is important information. + +1. Step two. +1. Step three. +``` + +## Table rules + +**Two-column tables:** +```markdown +Header 1 | Header 2 +----------------- | ---------------- +Content 1 | Content 2 +{: class="two-column"} +``` + +**Fixed-Width tables:** +```markdown +Header 1 | Header 2 +----------------- | ---------------- +Content 1 | Content 2 +{: class="fixed-width"} +``` + +**Responsive tables:** +```markdown +Header 1 | Header 2 +----------------- | ---------------- +Content 1 | Content 2 +{: class="responsive-table"} +``` + +## Code rules + +**Inline code:** +- Use single backticks: `code` +- Use for filenames, commands, and short code snippets +- Never use in headings + +**Code blocks:** +- Use triple backticks with language identifier: + ```yaml + steps: + - command: "echo hello" + ``` + +**Code block filenames:** +- Add filename after code block: `{: codeblock-file="filename.yml"}` + +**Emoji escaping in code:** +- Escape colons in emoji codes: `\:hammer\:` to prevent rendering +- Use when showing emoji codes in examples + +## Syntax highlighting + +**Supported languages:** +- Use Rouge syntax highlighting +- Common languages: `bash`, `yaml`, `json`, `javascript`, `ruby`, `python` + +## Content organization rules + +**Readability:** +- Use responsive tables for complex data +- Keep callouts concise +- Use appropriate list types (ordered vs unordered) +- Maintain consistent formatting throughout + +## Markdown syntax checklist + +- [ ] Headings nested incrementally (H1 → H2 → H3 → H4) +- [ ] Empty lines above and below headings +- [ ] Sentence case headings without punctuation +- [ ] `**bold**` for Buildkite UI elements, `_italics_` for key terms +- [ ] `-` for top-level lists, `*` for 2nd level, `-` for 3rd level +- [ ] 4-space indentation for nested lists and paragraphs +- [ ] `1.` for all numbered list items +- [ ] Relative URLs for internal links (`/docs/...`) +- [ ] Absolute URLs for external links +- [ ] Language identifiers in code blocks +- [ ] Escaped emoji in code examples when needed +- [ ] Appropriate table classes for styling +- [ ] One space after sentence punctuation + +--- + +# Buildkite YAML documentation rules + +## General YAML writing guidelines + +**Refer to meaning, not source:** +- Describe what the YAML represents (command, step, pipeline) rather than the literal YAML syntax +- Never use code formatting when referring to an abstract meaning +- Use code formatting only for literal YAML source or filenames + +**Examples:** +- Correct: "Here is an example pipeline configuration…" +- Correct: "Add this step to the pipeline…" +- Incorrect: "Add a `command` to `pipeline.yml`…" + +**Avoid YAML specification terminology:** +- Never use: *block*, *flow*, *sequence*, *scalar* as these terms conflict with product terminology or are unclear to users + +## YAML code formatting rules + +**Always use block-style:** +- Use block-style maps and arrays only +- Never use inline/flow style maps and arrays +- Block, quoted, and unquoted strings are all acceptable + +**Correct block style:** +```yaml +steps: + - label: Tests + command: + - npm install + - npm test +``` + +**Correct multi-line string:** +```yaml +steps: + - label: "Tests" + command: > + npm run test-runner -- + --with=several + --arguments + --split-across-lines-for-readability +``` + +**Incorrect inline style:** +```yaml +{ steps: [ label: "Tests", command: "npm test" ]} +``` + +## YAML terminology rules + +### Map +**Definition:** A collection of key-value pairs (associative arrays, dictionaries, or objects) + +**Usage:** +- Use "map" (noun) only for collections of key-value pairs +- Never use "map" to refer to individual keys or values +- Avoid terms like "block", "section", or "property" + +**Examples:** +- Correct: "A command is a map that configures…" +- Incorrect: "Add the `matrix` map to the…" + +**Nested Maps:** +- Use "map of maps" or "nested map" +- Qualify relationships with "of" +- Avoid "block", "level", or "sub-" prefix + +**Examples:** +- Correct: "The `retry` attribute of a step is a map of maps…" +- Incorrect: "The sub-block contains…" + +### Attribute +**Definition:** A key-value pair as a complete unit (not the identifier or value alone) + +**Usage:** +- Use "attribute" to refer to the entire key-value pair +- Never use "attribute" for just the key or just the value + +**Examples:** +- Correct: "The `steps` attribute determines…" +- "Add the `steps` attribute to the command step, then on a new line…" + +### Key and value +**Definition:** +- Key: The identifier part of an attribute +- Value: The data part of an attribute + +**Usage:** +- Use "key" only for the identifier +- Use "value" only for the data +- Always use code formatting for literal keys or values + +**Examples:** +- Correct: "Add the `skip` key to the command step, then on a new line…" +- Correct: "Set the value to `true`, `false`, or a string…" +- Incorrect: "Add the skip key to the command step…" (missing code formatting) + +### Array +**Definition:** A sequence or list of items + +**Usage:** +- Use "array" only for sequences/lists +- Never use "array" for maps +- Avoid terms like "list" or "entries" + +**Examples:** +- Correct: "The step attribute consists of an array of step maps…" +- Incorrect: "The attribute contains a list of entries…" + +## YAML code examples rules + +**Code block requirements:** +- Always use `yaml` language identifier +- Use proper indentation (2 spaces is standard) +- Show realistic, complete examples +- Include context when necessary + +**Documentation format:** +```yaml +steps: + - label: "Example step" + command: "echo 'Hello World'" + key: "example-step" +``` + +## YAML reference checklist + +- [ ] Describe YAML meaning, not literal syntax +- [ ] Use code formatting for literal YAML source and filenames +- [ ] Use block-style formatting in all examples +- [ ] Use correct terminology: map, attribute, key, value, array +- [ ] Avoid YAML spec terms: block, flow, sequence, scalar +- [ ] Include `yaml` language identifier in code blocks +- [ ] Use 2-space indentation in YAML examples +- [ ] Qualify nested relationships with "of" or "nested" +- [ ] Never use inline/flow style formatting + +## Sensitive information security + +### Never include in examples + +**Personal/account information:** +- Real API tokens or keys +- Real email addresses +- Real passwords or credentials +- Real account IDs or organization IDs +- Real webhook URLs or endpoints +- Real IP addresses (use RFC 5737 ranges: 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) + +**Buildkite-specific:** +- Real agent tokens +- Real GraphQL/REST API tokens +- Real build artifacts URLs +- Real SSO/SAML credentials or endpoints +- Real cluster tokens or queue names from production + +**Safe Example Patterns:** +- API tokens: `xxx-yyy-zzz` or `YOUR_API_TOKEN` +- Emails: `user@example.com`, `admin@example.org` +- Organizations: `acme-inc`, `example-org`, `your-organization` +- Pipelines: `example-pipeline`, `your-pipeline` +- Webhook URLs: `https://example.com/webhook` +- Build IDs: `01234567-****-****-****-456789abcdef` + +**Validation Rules:** +- Check all code examples for realistic-looking tokens +- Verify URLs point to example.com or localhost +- Ensure UUIDs/IDs are clearly placeholder values +- Confirm no internal Buildkite infrastructure details +- Review any copied content for accidental real data + +--- + +If asked to rewrite a document based on these instructions: +Use natural writing style. Write like a human technical writer, not like AI. AVOID AI WRITING PATTERNS! diff --git a/CODEOWNERS b/CODEOWNERS index fdaa376ab4a..d65eecf5296 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,10 +1,7 @@ @dannymidnight -pages/ @mbelton-buildkite @gilesgas -data/ @mbelton-buildkite @gilesgas -images/ @mbelton-buildkite @gilesgas -vale/ @mbelton-buildkite @gilesgas -styleguide/ @mbelton-buildkite @gilesgas - -app/assets/ @buildkite/growth-activate -app/views/ @buildkite/growth-activate +pages/ @gilesgas +data/ @gilesgas +images/ @gilesgas +vale/ @gilesgas +styleguide/ @gilesgas diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..97a79a4a7b3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,269 @@ +# Contributing to the Buildkite Docs + +This guide provides details on how to work with the Buildkite Docs source code, which generates the [Buildkite Docs](https://buildkite.com/docs/) web site, and make contributions to its content. + +Please also note the following style guides, which are relevant to adding content to pages within this site: + +- [Writing style guide](./styleguides/writing-style.md) +- [Markdown syntax style guide](./styleguides/markdown-syntax-style.md) + +## Working with the docs site + +The Buildkite docs is a custom-built website. This section gives some guidance on working with the setup. + +As a public contributor to the Buildkite Docs, you should work with a fork of [this upstream repository](https://github.com/buildkite/docs) in your own GitHub account, and then create pull requests to this upstream. + +### Add a new docs page and nav entry + +To add a new documentation (docs) page and a nav entry for it: + +1. Create the file as a new Markdown file (with the extension `.md`) within the appropriate `pages` directory. Ensure the file name is written in all lowercase letters, and separate words using underscores. (These underscores will be automatically converted to hyphens when the Buildkite Docs site is rebuilt.) + +1. Add a corresponding entry to this new page in the [`./data/nav.yml`](./data/nav.yml) file, which adds a new entry for this page in the page navigation sidebar (nav) of the [Buildkite Docs site](https://buildkite.com/docs). Note the existing page entries in `nav.yml` and use them as a guide to determine the location and hence, placement of the entry to your new Markdown file (in the nav and `nav.yml`). The following elements require considering for a new entry in `nav.yml`: + + | Key | Description | Data type | + | ------------- | ----------- | --------- | + | `name` | Nav entry name (see note below). | String, required | + | `path` | Enter a relative URL path for internal pages. You can also prepend with `https` for external pages, or `mailto:` for email links, although this practice should be avoided or minimized. If the `path` is empty or omitted, then this will be rendered as a toggle that opens a new section of pages. | String, optional | + | `icon` | Prepend with an icon. | String, optional | + | `theme` | WIP: doesn't work yet. Apply a theme. You can use `green` or `purple`. | String, optional | + | `children` | Child nav entry items. | Array of objects, optional | + | `pill` | Append a pill to indicate the status of a page and its content. Currently, using `beta`, `coming-soon`, `deprecated` or `new` will generate pills that have color formatting. You can also use the pill `preview` to indicate that a page's content, along with the feature it documents, is still in development. | String, optional | + | `new_window` | Make this link open up a new window, although this practice should be avoided or minimized. | Bool, optional | + | `type` | Special nav link types. With `dropdown` the children nav items will be rendered as hover dropdown menus on laptop/desktop screen devices. `link` is a shortcut link that takes the user from one section to another (for example, you may link to SSO under the Integrations section from Pipeline's sidebar). It also renders an 'external link' icon as an affordance. Lastly, `divider` makes a divider line in the nav to help with visual delineation. | String, `dropdown|link|divider`, optional | + +> [!NOTE] +> Whenever you save changes to the `nav.yml` file, you'll need to stop and restart your local development environment in order to see these changes reflected in the nav. +> +> The Buildkite Docs web site is kept running with Ruby, which interprets underscores in filenames as hyphens. Therefore, if a page is called `octopussy_cat.md`, then for its entry in the `nav.yml` file, you need to reference its `path` key value as `octopussy-cat`. +> +> If you're creating a new section for the nav, then as described for the `path` key above, add the `name` key for this entry, omit its `path` key, and add a `children` key to create this new section. Then, nest/indent all new page entries within this section entry. +> +> Since a new section entry in the nav is purely a toggle that cannot hold page content itself, then to introduce a page for this new section, create a top-level "Overview" page for this section instead. + +### Linting + +This section describes the various linting features which are run as part of the Buildkite Docs build pipeline. + +The docs (in US English) are spell-checked and a few automated checks for repeated words, common errors, and Markdown and filename inconsistencies are also run. + +You can run most of these checks locally with [`./scripts/vale.sh`](./scripts/vale.sh). + +#### Markdown files + +Markdown files must be located within `pages`, and their names must be written in [`snake_case`](https://simple.wikipedia.org/wiki/Snake_case) and end with the `.md` extension. + +The [`.ls-lint.yml`](.ls-lint.yml) linter file contains rules that the ls-lint filename linter checks are observed. +Learn more about the [ls-lint filename linter](https://ls-lint.org/1.x/getting-started/introduction.html). + +#### Markdown content + +A [Markdown linter](https://github.com/DavidAnson/markdownlint) also runs on the Buildkite documentation's Markdown file content. + +The rules enabled for this Markdown linting are defined in the [`.markdownlint.yaml`](.markdownlint.yaml) file. + +For the linter jobs to pass, every line in a Markdown file must not end in any trailing spaces, and the last character in the Markdown file must be a new line character. + +#### Fix spelling errors + +The Buildkite Docs build pipeline uses [Vale](https://vale.sh/) to check for spelling errors, and builds will fail if a spelling error is encountered. Vale also checks for incorrect letter case handling, for example, Proper Nouns that should be treated as common nouns. + +If you need to add an exception to this (for example, you are referencing a new technology or tool that isn't in Vale's vocabulary), add this term verbatim to the [`./vale/styles/vocab.txt`](./vale/styles/vocab.txt) file, ensuring that the term is added in the correct alphabetical order within the file. Case is important but should be ignored with regard to alphabetical ordering within the file. This makes it easier to identify if an exception has already been added. + +If you encounter a spelling or letter case handling error within a heading, add this entry into the [`./vale/styles/Buildkite/h1-h6_sentence_case.yml`](./vale/styles/Buildkite/h1-h6_sentence_case.yml) file. + +#### Escape vale linting + +If you absolutely need to add some word or syntax that would trigger the linter into failing the docs build pipeline, you can use escaping using the following syntax: + +``` + + +This is some text that you do NOT want the linter to check + + +``` + +Use the `vale off` syntax before a phrase that needs to be bypassed by the linter and don't forget to turn it on again with `vale on`. + +### Content reuse (snippets/partials) + +You can use snippets (also known as partials) to reuse the same fragment of text in several documentation pages (single sourcing). This way, you can update the snippet once, and the changes will be visible on all pages that use this snippet. + +Add snippet files to appropriate locations within the `/pages` directory, prefaced with an underscore in the file name. For example `_my_snippet.md`. **However**, when pulling the snippet into a file, remove the leading underscore. + +This way, the following example snippet file located immediately within the `/pages` directory: + +`_step_2_3_github_custom_status.md` + +is referenced using this snippet render link: + +`<%= render_markdown partial: 'step_2_3_github_custom_status' %>` + +Use the snippet render link wherever you need to add the content of the snippet (multiple times if required) in other Markdown files throughout the Buildkite Docs. + +If a snippet is stored within a subdirectory of `/pages`, you need to specify the subdirectory hierarchy in the link to the snippet. + +Therefore, a reference to the `_agent_events_table.md` file stored within the `webhooks/pipelines` subdirectory of the `apis` subdirectory would look like this: + +`<%= render_markdown partial: 'apis/webhooks/pipelines/agent_events_table' %>` + +> [!WARNING] +> **The snippets/partials feature currently has the following limitations** +> - Headings are not supported. Using H2, H3-level headings within the snippet content can lead to incorrect anchor links being generated for them. Additionally, using any heading level within a snippet prevents these headings from appearing in the **On this page** feature. Instead, add the heading to the main document just before you add a snippet render link, with the snippet only containing the text content you want to reuse. +> - Snippets don't support conditional content. You cannot use variables to represent conditional content within a snippet. If you have content in a snippet where some of its content (such as a small number of words) needs to be changed depending on where the snippet is used, for example, a product name, you'll either need to create multiple snippets for each usage and reference them accordingly on their respective pages, or alternatively, write the content directly into their respective pages. + +### Custom elements + +The Buildkite docs has a few custom scripts for adding useful elements that are missing in Markdown. +To save yourself a few unnecessary rounds of edits in the future, remember that if you see a fragment written in HTML, links within such fragment should also follow the HTML syntax and not Markdown (more on this in [Note blocks](#note-blocks)). + +#### Beta flags + +To mark a content page in the site as being in beta, add its relative path _after_ `docs` to the [`./app/models/beta_pages.rb`](./app/models/beta_pages.rb) file. + +For example: +``` +[ + 'pipelines/some-new-beta-feature', + 'test-engine/some-new-beta-feature', + 'package-registries/some-new-beta-feature' +] +``` + +Any file listed there will automatically pick up the beta styling. + +Adding the class `has-pill-beta` to any element will append the beta pill. This is intended for use in the sidebar and homepage navigation and will not work in Markdown. + +#### Table of contents + +An in-page table of contents (with the **On this page** title) is automatically generated based on `##`-level headings. + +You can omit a table of contents by adding some additional metadata to a Markdown template using the following YAML front matter: + +```yaml +--- +toc: false +--- +``` + +#### Prepend icons + +You can prepend an icon to boost the visual emphasis for an inline text. To do this, wrap the text with ``. + +At the time of writing, there are only three icons available — agent, repository, and plugin. To add more icons see `$icons` in `_add-icon.scss`, add a new name as the key and the inline SVG. Icon dimension must be 22px * 22px. + +> [!NOTE] +> Unlike emojis, these icons are generic and contextual, and they are used as to help readers to better visually differentiate specific terms from the rest of the text. + +### Update Buildkite Agent CLI docs + +The [Buildkite Agent command-line interface (CLI) reference docs](https://buildkite.com/docs/agent/v3/cli/reference) consists of a series of pages where each page describes how each of the agent's `buildkite-agent` CLI commands works and is used. + +Each command's docs page should have a **Usage**, **Description**, **Example**, and **Options** section appearing somewhere on the page. + +These four sections are actually part of a [partial](#content-reuse-snippetspartials), whose content comes from its relevant Markdown file partial in the [docs source repo's `pages/agent/v3/help` folder](./pages/agent/v3/help). The files in this folder are automatically updated whenever a [new version of the Buildkite Agent is released](https://github.com/buildkite/agent/releases), containing updates to the documentation in any of its relevant [clicommand files](https://github.com/buildkite/agent/tree/main/clicommand). This is why the tops of these file partials indicate **DO NOT EDIT**. + +With the development dependencies installed you can update these CLI docs locally with the following: + +```bash +# Set a custom PATH to select a locally built buildkite-agent +PATH="$HOME/Projects/buildkite/agent:$PATH" ./scripts/update-agent-help.sh +``` + +### Update the GraphQL API docs + +The GraphQL API reference documentation (from the start of [Queries](https://buildkite.com/docs/apis/graphql/schemas/query/agent) through to end of [Unions](https://buildkite.com/docs/apis/graphql/schemas/union/usageunion)) is generated from a local version of the [Buildkite GraphQL API schema](./data/graphql/schema.graphql). + +This repository is kept up-to-date with production based on a daily scheduled build that generates a pull request. The build fetches the latest GraphQL schema from the Buildkite API, generates the documentation, and publishes a pull request for review. + +If you need to fetch the latest schema, you can run the following in your local environment: + +```sh +# Fetch latest schema +API_ACCESS_TOKEN=xxx bundle exec rake graphql:fetch_schema >| data/graphql/schema.graphql + +# Generate docs based on latest schema +bundle exec rake graphql:generate +``` + +### Update vendor/emojis + +From time to time, you will start seeing an update to `vendor/emojis` submodule as a default initial file change in every new branch you create. This happens because these new branches will have an older version of the emoji submodule than the main branch. + +**Do not commit the `vendor/emojis` commit!** Instead, run `git submodule update`. This will take care of the emoji commit - until your local emoji submodule version falls behind again. Then you will need to run `git submodule update` for your local Docs repository again. + +If you do accidentally commit the `vendor/emojis` update, use `git reset --soft HEAD~1` to undo your last commit, un-stage the erroneous submodule change, and commit again. + +### Search index + +**Note:** By default, search (through Algolia) references the production search index. + +The search index is updated once a day by a scheduled build using the config in `config/algolia.json`. + +To test changes to the indexing configuration: + +1. Make sure you have an API key in `.env` like: + + ```env + APPLICATION_ID=APP_ID + API_KEY=YOUR_API_KEY + ``` + +2. Run `bundle exec rake update_test_index`. + +### Content keywords + +Content keywords are rendered in `data-content-keywords` in the `body` tag to highlight the focus keywords of each page with content authors. + +This helps the main documentation contribution team quickly inspect to see the types of content Buildkite provides across different channels. + +Keywords are added as [Frontmatter](https://rubygems.org/gems/front_matter_parser) meta data using the `keywords` key, e.g.: + +```md +keywords: docs, tutorial, pipelines, 2fa +``` + +If no keywords are provided it falls back to comma-separated URL path segments. + +## Screenshots + +This information was aggregated by going over the existing screenshots in the documentation repo. Feel free to change or expand it. + +### Taking and processing screenshots + +* **Format:** PNG +* **Ratio:** arbitrary, but **strictly even number of pixels** for both height and width. Recommended size `width: 1024px, height: 880px` when you're taking a full-width screen +* **Size:** the largest possible resolution that makes sense. It's preferable that you take the screenshots on a Mac laptop with a Retina or high-resolution display/screen. Recommended dimension is `width: 2048/2, height: 880/2` to get the best possible view across different screen sizes. +* **No feature flag:** please remember to turn off all experimental features when taking screenshots +* **Border:** no border +* **Drop shadow:** no +* **Cursor:** include when relevant +* **Area highlight selection:** subtract overlay +* **Blur:** use to obscure sensitive info like passwords or real email addresses; even, non-pixelated +* **User info:** blur out everything except for the name +* **Dummy data:** use Acme Inc as dummy company title +* **Naming screenshots:** lowercase, words separated by hyphens; number after the title, for example, "installation-1" + +### Adding screenshots or other images + +> Before you proceed, make sure that both the width and the height of the image are an even number of pixels! + +Steps for adding add an image to a documentation page: + +1. Name the image file (lowercase, separate words using hyphens; add a number to the filename, for example, 'installation-1' if you are adding several images to the same page). + +1. Save the file into its corresponding folder within `/images/docs`. This folder is a sub-folder within `/images/docs` whose path matches that of the Markdown page's path within `/pages`, _which includes_ the file name of Markdown page that this image file is referenced on, as the final sub-folder. Create this sub-folder hierarchy if it doesn't yet exist within `/images/docs`. + + For example, if you add an image called `my_image.png` to a page located in the path `/pages/pipelines/insights/queue_metrics.md`, then save the actual image file to the path `/images/docs/pipelines/insights/queue_metrics/my_image.png`. + +1. Compose relevant alt text for the image file using sentence case. + +1. Add your image file to the documentation page using the following code example `<%= image "your-image.png", width: 1110, height: 1110, alt: "Screenshot of Important Feature" %>`. +For large images/screenshots taken on a retina screen, use `<%= image "your-image.png", width: 1110/2, height: 1110/2, alt: "Screenshot of Important Feature" %>`. + +## Talking about YAML + +YAML looks more simple than it is. +It takes some care and discipline to write about. +See [Talking about YAML](./yaml.md) for complete guidance. diff --git a/Dockerfile b/Dockerfile index bbcd1d2b0fb..b24040cd8b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -ARG BASE_IMAGE=public.ecr.aws/docker/library/ruby:3.3.3-slim-bookworm@sha256:bc6372a998e79b5154c8132d1b3e0287dc656249f71f48487a1ecf0d46c9c080 -ARG NODE_IMAGE=public.ecr.aws/docker/library/node:18-bookworm-slim@sha256:d2d8a6420c9fc6b7b403732d3a3c5395d016ebc4996f057aad1aded74202a476 +ARG BASE_IMAGE=public.ecr.aws/docker/library/ruby:3.4.5-slim-bookworm@sha256:aa97c41012d81fa89ab5cf61409c3314665d024fd06c3af1ecea27865ffbd9c4 +ARG NODE_IMAGE=public.ecr.aws/docker/library/node:22-bookworm-slim FROM $BASE_IMAGE AS builder @@ -11,7 +11,7 @@ RUN echo "--- :package: Installing system deps" \ && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ # Install a few pre-reqs && apt-get update \ - && apt-get install -y curl gnupg \ + && apt-get install -y curl gnupg libyaml-dev \ # Setup apt for GH cli && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ @@ -42,14 +42,22 @@ RUN echo "--- :bundler: Installing ruby gems" \ # ------------------------------------------------------------------ -FROM $NODE_IMAGE as node-deps +FROM $NODE_IMAGE AS node-deps COPY package.json yarn.lock ./ RUN echo "--- :yarn: Installing node packages" && yarn # ------------------------------------------------------------------ -FROM builder as assets +FROM public.ecr.aws/docker/library/golang:1.24-bookworm AS gobuild + +# This was previously installed from gobinaries.com within +# the deploy-preview step, but gobinaries.com keeps being unavailable :( +RUN go install github.com/tj/staticgen/cmd/staticgen@v1.1.0 + +# ------------------------------------------------------------------ + +FROM builder AS assets COPY . /app/ COPY --from=node-deps /usr/local/bin /usr/local/bin @@ -68,10 +76,6 @@ RUN if [ "$RAILS_ENV" = "production" ]; then \ FROM $BASE_IMAGE AS runtime -# Install a few misc. deps for CI -RUN apt-get update && apt-get install -y curl jq -RUN apt purge --assume-yes linux-libc-dev - WORKDIR /app ARG RAILS_ENV @@ -97,3 +101,44 @@ RUN bundle exec rake sitemap:create EXPOSE 3000 CMD ["bundle", "exec", "puma", "-C", "./config/puma.rb"] + +# ------------------------------------------------------------------ +# +# We use this image to deploy previews to Netlify. +# +# It needs npm packages installed by Yarn in node-deps, as well as jq, curl and +# staticgen for our orchestration machinery. +# + +FROM runtime AS deploy-preview + +# bin/deploy-preview has a couple of dependencies +RUN apt-get update && \ + apt-get install -y curl jq && \ + apt purge --assume-yes linux-libc-dev + +COPY --from=gobuild /go/bin/staticgen /usr/local/bin/staticgen + +# ------------------------------------------------------------------ +# +# We use this image to run Muffet, a link checking tool. +# +# We use a Ruby wrapper script to process the results in ways that +# make sense to us. +# + +FROM raviqqe/muffet:2.11.0 AS muffet-scratch +FROM ${BASE_IMAGE} AS muffet + +RUN apt-get update && \ + apt-get install -y curl jq wget && \ + apt purge --assume-yes linux-libc-dev + +COPY --from=muffet-scratch /muffet /muffet + +# ------------------------------------------------------------------ +# +# Here, we ensure that the `runtime` image is the final result if this +# Dockerfile is invoked without specifying a target. +# +FROM runtime diff --git a/Gemfile b/Gemfile index 9f6ac4c776f..1aca3693d89 100644 --- a/Gemfile +++ b/Gemfile @@ -6,10 +6,10 @@ ruby File.read(".ruby-version").strip source "https://rubygems.org" # Choo choo šŸš (only include the Rails gems we need) -gem "actionpack", "~> 6.1" -gem "actionview", "~> 6.0" -gem "activesupport", "~> 6.1" -gem "railties", "~> 6.0" +gem "actionpack", "~> 8.0.1" +gem "actionview", "~> 8.0.1" +gem "activesupport", "~> 8.0.1" +gem "railties", "~> 8.0.1" # Use Puma as the app server gem "puma" @@ -27,7 +27,7 @@ gem "redcarpet" gem "commonmarker" # Syntax highlighting code -gem "rouge", "3.3.0" +gem "rouge", "3.30.0" # For escaping code snippets in markdown gem "escape_utils" @@ -74,8 +74,6 @@ group :test do # Browser testing stuff gem "capybara" -end -group :test do gem "buildkite-test_collector" end diff --git a/Gemfile.lock b/Gemfile.lock index 6b50c7268cc..353562e06f7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,36 +1,46 @@ GEM remote: https://rubygems.org/ specs: - actionpack (6.1.7.8) - actionview (= 6.1.7.8) - activesupport (= 6.1.7.8) - rack (~> 2.0, >= 2.0.9) + actionpack (8.0.1) + actionview (= 8.0.1) + activesupport (= 8.0.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actionview (6.1.7.8) - activesupport (= 6.1.7.8) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actionview (8.0.1) + activesupport (= 8.0.1) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.7.8) - activesupport (= 6.1.7.8) - globalid (>= 0.3.6) - activemodel (6.1.7.8) - activesupport (= 6.1.7.8) - activesupport (6.1.7.8) - concurrent-ruby (~> 1.0, >= 1.0.2) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activemodel (8.0.1) + activesupport (= 8.0.1) + activesupport (8.0.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) bindex (0.8.1) - bugsnag (6.21.0) + bugsnag (6.27.1) concurrent-ruby (~> 1.0) - builder (3.2.4) + builder (3.3.0) buildkite-test_collector (2.3.1) activesupport (>= 4.2) byebug (11.1.3) @@ -44,126 +54,158 @@ GEM xpath (~> 3.2) coderay (1.1.3) commonmarker (0.23.10) - concurrent-ruby (1.3.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) crass (1.0.6) - diff-lcs (1.4.4) + date (3.4.1) + diff-lcs (1.6.0) dotenv (2.8.1) dotenv-rails (2.8.1) dotenv (= 2.8.1) railties (>= 3.2) + drb (2.2.1) dry-cli (1.0.0) - erubi (1.12.0) + erubi (1.13.1) escape_utils (1.2.1) - foreman (0.87.2) + fiber-storage (1.0.0) + foreman (0.88.1) front_matter_parser (1.0.1) - globalid (1.1.0) - activesupport (>= 5.0) - graphql (2.0.16) - graphql-client (0.18.0) + graphql (2.4.13) + base64 + fiber-storage + logger + graphql-client (0.25.0) activesupport (>= 3.0) - graphql - i18n (1.14.5) + graphql (>= 1.13.0) + i18n (1.14.7) concurrent-ruby (~> 1.0) + io-console (0.8.0) + irb (1.15.1) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + logger (1.6.6) lograge (0.12.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.22.0) + loofah (2.24.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) matrix (0.4.2) method_source (1.0.0) mini_mime (1.1.0) - mini_portile2 (2.8.7) - minitest (5.23.1) - nio4r (2.7.0) - nokogiri (1.16.5) + mini_portile2 (2.8.9) + minitest (5.25.4) + nio4r (2.7.4) + nokogiri (1.18.9) mini_portile2 (~> 2.8.2) racc (~> 1.4) + pp (0.6.2) + prettyprint + prettyprint (0.2.0) pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) + psych (5.2.3) + date + stringio public_suffix (4.0.6) - puma (5.6.8) + puma (6.6.0) nio4r (~> 2.0) - racc (1.8.0) - rack (2.2.9) + racc (1.8.1) + rack (2.2.20) rack-proxy (0.7.6) rack - rack-test (2.1.0) + rack-session (1.0.2) + rack (< 3) + rack-test (2.2.0) rack (>= 1.3) + rackup (1.0.1) + rack (< 3) + webrick rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) - nokogiri (~> 1.14) - railties (6.1.7.8) - actionpack (= 6.1.7.8) - activesupport (= 6.1.7.8) - method_source + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) + irb (~> 1.13) + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rake (13.2.1) + rdoc (6.12.0) + psych (>= 4.0.0) redcarpet (3.6.0) regexp_parser (2.1.1) + reline (0.6.0) + io-console (~> 0.5) request_store (1.5.0) rack (>= 1.4) - rouge (3.3.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rouge (3.30.0) + rspec-core (3.13.3) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.10.2) + rspec-support (~> 3.13.0) + rspec-rails (7.1.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.2) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) + securerandom (0.4.1) sitemap_generator (6.3.0) builder (~> 3.0) - stringex (2.8.5) - thor (1.2.1) - turbo-rails (1.4.0) + stringex (2.8.6) + stringio (3.1.3) + thor (1.3.2) + turbo-rails (2.0.11) actionpack (>= 6.0.0) - activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - vite_rails (3.0.14) - railties (>= 5.1, < 8) + uri (1.0.4) + useragent (0.16.11) + vite_rails (3.0.19) + railties (>= 5.1, < 9) vite_ruby (~> 3.0, >= 3.2.2) vite_ruby (3.3.0) dry-cli (>= 0.7, < 2) rack-proxy (~> 0.6, >= 0.6.1) zeitwerk (~> 2.2) - web-console (4.2.0) + web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webrick (1.9.1) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.15) + zeitwerk (2.7.1) PLATFORMS ruby DEPENDENCIES - actionpack (~> 6.1) - actionview (~> 6.0) - activesupport (~> 6.1) + actionpack (~> 8.0.1) + actionview (~> 8.0.1) + activesupport (~> 8.0.1) bugsnag buildkite-test_collector byebug @@ -178,9 +220,9 @@ DEPENDENCIES matrix pry puma - railties (~> 6.0) + railties (~> 8.0.1) redcarpet - rouge (= 3.3.0) + rouge (= 3.30.0) rspec-rails rspec_junit_formatter sitemap_generator @@ -190,7 +232,7 @@ DEPENDENCIES web-console RUBY VERSION - ruby 3.3.3p89 + ruby 3.4.5p51 BUNDLED WITH - 2.4.7 + 2.6.3 diff --git a/Procfile b/Procfile index b9b4dbb493d..cdfb7353cf2 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ vite: bin/vite dev -web: bin/rails s -p 3000 +web: bin/rails s -p ${WEB_PORT-3000} diff --git a/README.md b/README.md index c43cdfb2bd6..5788879e7f3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Buildkite Documentation [![Build status](https://badge.buildkite.com/b1b9e3ef9d893c087f5e5c0a2d04c258ba393bed2379273f63.svg?branch=main)](https://buildkite.com/buildkite/docs) -The source files for the [Buildkite Documentation](https://buildkite.com/docs). +The source files for the [Buildkite Documentation](https://buildkite.com/docs), aka the Buildkite Docs, or just docs. To contribute, please send a pull request! :heart: -## Development +## Local docs development environment ### Before you start @@ -26,7 +26,7 @@ On some platforms (for example, Linux-based ones), you may need to prefix `docke #### Get the Buildkite Docs source -Clone the Buildkite Docs source locally. To do so, run these commands: +As a public contributor to the Buildkite Docs, clone its source repository locally. To do so, run these commands: ```bash git clone git@github.com:buildkite/docs.git @@ -38,7 +38,7 @@ git submodule update --init ### Run the development server -After completing the relevant 'Before you start' steps above: +After completing all the relevant [Before you start](#before-you-start) steps above: 1. Build and run your local Buildkite Docs development server environment. @@ -54,11 +54,14 @@ After completing the relevant 'Before you start' steps above: # Install dependencies bin/setup - # Start the app + # Start the app on port 3000 foreman start + + # Alternatively, to start the app on a port other than 3000 (e.g. 3010) + WEB_PORT=3010 foreman start ``` - **Note:** After stopping the non-containerized server, simply run `foreman start` to re-start the server again. If, however, the `foreman start` command fails to run successfully, try re-running the `bin/setup` command again to update any dependencies before running `foreman start` again. + **Note:** After stopping the non-containerized server, simply run your `foreman start` command again to re-start the server again. If, however, the `foreman start` command fails to run successfully, try re-running the `bin/setup` command again to update any dependencies before running your `foreman start` command again. For containerized development, run the following: @@ -67,99 +70,24 @@ After completing the relevant 'Before you start' steps above: docker-compose up --build ``` -1. Open `http://localhost:3000` to preview the docs site. +1. Open `http://localhost:3000` (or your chosen port number) to preview the docs site. 1. After saving your modifications to a page, refresh the relevant page on this site to see your changes. > [!NOTE] > If you ever make more significant changes than just page updates (for example, adding a new page), you may need to stop and restart the Buildkite Docs development server to see these changes. -## Updating `buildkite-agent` CLI docs - -With the development dependencies installed you can update the CLI docs with the following: - -```bash -# Set a custom PATH to select a locally built buildkite-agent -PATH="$HOME/Projects/buildkite/agent:$PATH" ./scripts/update-agent-help.sh -``` - -## Updating GraphQL API docs - -GraphQL API documentation is generated from a local version of the [Buildkite GraphQL API schema](./data/graphql/schema.graphql). - -This repository is kept up-to-date with production based on a [daily scheduled build](https://buildkite.com/buildkite/docs-graphql). The build fetches the latest GraphQL schema, generates the documentation, and publishes a pull request for review. - -If you need to fetch the latest schema you can either: - -- Manually trigger a build on [`buildkite/docs-graphql`](https://buildkite.com/buildkite/docs-graphql); or -- Run the following in your local environment: - -```sh -# Fetch latest schema -API_ACCESS_TOKEN=xxx bundle exec rake graphql:fetch_schema >| data/graphql/schema.graphql - -# Generate docs based on latest schema -bundle exec rake graphql:generate -``` - -## Linting - -We spell-check the docs (US English) and run a few automated checks for repeated words, common errors, and markdown and filename inconsistencies. - -You can run most of these checks with `./scripts/vale.sh`. - -If you've added a new valid word that showing up as a spelling error, add it to `./vale/styles/vocab.txt`. - -## Style guides +Learn how to contribute to the Buildkite Docs in the [CONTRIBUTING guide](./CONTRIBUTING.md). -Our documentation is based on the principles of common sense, clarity, and brevity. +## Contributing to the docs and style guides -The [writing](/styleguides/writing-style.md) and [Markdown syntax](/styleguides/markdown-syntax-style.md) style guides should provide you a general idea and an insight into our language and writing style, as well as the Markdown syntax we use (including custom formatting elements). +The Buildkite Docs is based on the principles of common sense, clarity, and brevity. -## Search index +Refer to the: -**Note:** By default, search (through Algolia) references the production search index. - -The search index is updated once a day by a scheduled build using the config in `config/algolia.json`. - -To test changes to the indexing configuration: - -1. Make sure you have an API key in `.env` like: - - ```env - APPLICATION_ID=APP_ID - API_KEY=YOUR_API_KEY - ``` - -2. Run `bundle exec rake update_test_index`. - -## Updating the navigation - -The navigation is split into the following files: - -- `nav_graphql.yml`: For the GraphQL API content. -- `nav.yml`: For everything else. - -A combined navigation is generated when the application starts. - -Otherwise, to update the general navigation: - -1. Edit `nav.yml` with your changes. -1. Restart the application. - -## Content keywords - -We render content keywords in `data-content-keywords` in the `body` tag to highlight the focus keywords of each page with content authors. - -This helps the content team to quickly inspect to see the types of content we're providing across different channels. - -Keywords are added as [Frontmatter](https://rubygems.org/gems/front_matter_parser) meta data using the `keywords` key, e.g.: - -```md -keywords: docs, tutorial, pipelines, 2fa -``` +- [Contributing to the Buildkite Docs](CONTRIBUTING.md) guide for details on how to start making a contribution in a new pull request. -If no keywords are provided it falls back to comma-separated URL path segments. +- [Writing](/styleguides/writing-style.md) and [Markdown syntax](/styleguides/markdown-syntax-style.md) style guides, which should provide a general idea and an insight into the language and writing style used throughout the Buildkite Docs, as well as the Markdown syntax used (including custom formatting elements). ## License diff --git a/app/controllers/llm_text_controller.rb b/app/controllers/llm_text_controller.rb new file mode 100644 index 00000000000..cf1f56a9d95 --- /dev/null +++ b/app/controllers/llm_text_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class LLMTextController < ApplicationController + def index + content = LLMText.generate + + render plain: content, content_type: "text/plain" + end +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 0dc316f1b32..c2385ff65f4 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -24,8 +24,13 @@ def show return # ensure we exit the method after redirecting end - # Otherwise, render the page (the default) - render @page.template + # Handle different formats + respond_to do |format| + format.html { render @page.template } + format.md { + render plain: @page.markdown_body, content_type: "text/markdown" + } + end end private diff --git a/app/controllers/quick_reference_controller.rb b/app/controllers/quick_reference_controller.rb index 930a8dd6e44..8c92a6f21c7 100644 --- a/app/controllers/quick_reference_controller.rb +++ b/app/controllers/quick_reference_controller.rb @@ -4,16 +4,16 @@ class QuickReferenceController < ApplicationController before_action :add_cors_headers PIPELINE_PAGES = [ - 'pipelines/command_step', - 'pipelines/wait_step', - 'pipelines/block_step', - 'pipelines/input_step', - 'pipelines/trigger_step', - 'pipelines/group_step' + 'pipelines/configure/step-types/command_step', + 'pipelines/configure/step-types/wait_step', + 'pipelines/configure/step-types/block_step', + 'pipelines/configure/step-types/input_step', + 'pipelines/configure/step-types/trigger_step', + 'pipelines/configure/step-types/group_step' ].freeze NOTIFICATION_PAGES = [ - 'pipelines/notifications' + 'pipelines/configure/notifications' ].freeze def pipelines diff --git a/app/frontend/components/copyToClipboardButton.js b/app/frontend/components/copyToClipboardButton.js index d4d9d50f971..34d8184ab82 100644 --- a/app/frontend/components/copyToClipboardButton.js +++ b/app/frontend/components/copyToClipboardButton.js @@ -6,7 +6,7 @@ const checkedCircleIcon = ` const clipboardDocumentIcon = ` - + `; const COPY_TIMEOUT = 600; diff --git a/app/frontend/components/nav.js b/app/frontend/components/nav.js index be2e85b4890..95e7e826c16 100644 --- a/app/frontend/components/nav.js +++ b/app/frontend/components/nav.js @@ -42,3 +42,24 @@ export function bindToggles() { }) ); } + +export function preserveScroll() { + const navContainer = document.querySelector(".Page__sidebar"); + const storageKey = "navScrollTop"; + + if (!navContainer) { + return; + } + + // On page load, restore the scroll position from sessionStorage + const savedScrollTop = sessionStorage.getItem(storageKey); + if (savedScrollTop) { + navContainer.scrollTop = parseInt(savedScrollTop, 10); + } + + // Before leaving the page, save the current scroll position. + // turbo:before-visit is the Turbo event for this. + document.addEventListener("turbo:before-visit", () => { + sessionStorage.setItem(storageKey, navContainer.scrollTop); + }); +} diff --git a/app/frontend/components/pageCopyDropdown.js b/app/frontend/components/pageCopyDropdown.js new file mode 100644 index 00000000000..47c1eb9c890 --- /dev/null +++ b/app/frontend/components/pageCopyDropdown.js @@ -0,0 +1,348 @@ +// Configuration constants +const COPY_TIMEOUT = 2000; +const ERROR_TIMEOUT = 3000; +const PRODUCTION_BASE_URL = "https://buildkite.com"; + +// AI service configuration +const AI_SERVICES = { + chatgpt: { + url: "https://chat.openai.com/", + queryParam: "q", + }, + claude: { + url: "https://claude.ai/new", + queryParam: "q", + }, + // Removed until we have have hosted MCP + // cursor: { + // url: 'cursor://anysphere.cursor-deeplink/mcp/install?name=buildkite&config=eyJjb21tYW5kIjoiZG9ja2VyIHJ1biAtaSAtLXJtIC1lIEJVSUxES0lURV9BUElfVE9LRU4gZ2hjci5pby9idWlsZGtpdGUvYnVpbGRraXRlLW1jcC1zZXJ2ZXIgc3RkaW8iLCJlbnYiOnsiQlVJTERLSVRFX0FQSV9UT0tFTiI6ImJrdWFfeHh4eHh4eHgifX0%3D' + // } +}; + +export function initPageCopyDropdown() { + const dropdown = document.querySelector(".page-copy-dropdown"); + if (!dropdown) return null; + + // AbortController for cleanup + const abortController = new AbortController(); + const { signal } = abortController; + + // Move dropdown to be inline with the first h1 heading + positionDropdownWithHeading(dropdown); + + // Get DOM elements + const button = dropdown.querySelector(".page-copy-dropdown__button"); + const menu = dropdown.querySelector(".page-copy-dropdown__menu"); + const copyButton = dropdown.querySelector('[data-action="copy-markdown"]'); + const viewButton = dropdown.querySelector('[data-action="view-markdown"]'); + const aiButtons = dropdown.querySelectorAll( + '[data-action^="open-"], [data-action="connect-cursor"]' + ); + + if (!button || !menu) return null; + + // Dropdown state management + function closeDropdown() { + menu.classList.remove("page-copy-dropdown__menu--open"); + button.setAttribute("aria-expanded", "false"); + } + + function openDropdown() { + menu.classList.add("page-copy-dropdown__menu--open"); + button.setAttribute("aria-expanded", "true"); + } + + function toggleDropdown() { + const isOpen = menu.classList.contains("page-copy-dropdown__menu--open"); + if (isOpen) { + closeDropdown(); + } else { + openDropdown(); + } + } + + // Event handlers + const handleButtonClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + toggleDropdown(); + }; + + const handleOutsideClick = (e) => { + if (!dropdown.contains(e.target)) { + closeDropdown(); + } + }; + + const handleKeyboardNavigation = (e) => { + const isOpen = menu.classList.contains("page-copy-dropdown__menu--open"); + + switch (e.key) { + case "Escape": + if (isOpen) { + e.preventDefault(); + closeDropdown(); + button.focus(); + } + break; + case "ArrowDown": + case "ArrowUp": + if (isOpen) { + e.preventDefault(); + navigateMenuItems(e.key === "ArrowDown" ? 1 : -1); + } + break; + } + }; + + function navigateMenuItems(direction) { + const focusableElements = menu.querySelectorAll("button:not(:disabled)"); + if (focusableElements.length === 0) return; + + const currentIndex = Array.from(focusableElements).findIndex( + (el) => el === document.activeElement + ); + let nextIndex; + + if (direction === 1) { + // ArrowDown + nextIndex = + currentIndex === -1 ? 0 : (currentIndex + 1) % focusableElements.length; + } else { + // ArrowUp + nextIndex = + currentIndex === -1 + ? focusableElements.length - 1 + : (currentIndex - 1 + focusableElements.length) % + focusableElements.length; + } + + focusableElements[nextIndex].focus(); + } + + // Add event listeners with cleanup support + button.addEventListener("click", handleButtonClick, { signal }); + document.addEventListener("click", handleOutsideClick, { signal }); + dropdown.addEventListener("keydown", handleKeyboardNavigation, { signal }); + + // Copy to clipboard functionality + if (copyButton) { + copyButton.addEventListener("click", handleCopyMarkdown, { signal }); + } + + // View markdown functionality + if (viewButton) { + viewButton.addEventListener("click", handleViewMarkdown, { signal }); + } + + // AI integration buttons + aiButtons.forEach((button) => { + button.addEventListener("click", handleAIIntegration, { signal }); + }); + + async function handleCopyMarkdown(e) { + e.preventDefault(); + closeDropdown(); + + const titleElement = copyButton.querySelector( + ".page-copy-dropdown__item-title" + ); + const icon = copyButton.querySelector(".page-copy-dropdown__item-icon svg"); + const originalTitle = titleElement?.textContent || "Copy as Markdown"; + + try { + // Show loading state + setButtonState(copyButton, "loading", "Copying...", icon); + + const markdownContent = await fetchMarkdownContent(); + await copyToClipboard(markdownContent); + + // Show success state + setButtonState(copyButton, "success", "Copied!", icon); + + // Reset after delay + setTimeout(() => { + resetButtonState(copyButton, originalTitle, icon); + }, COPY_TIMEOUT); + } catch (error) { + console.error("Failed to copy markdown:", error); + + // Show error state + setButtonState(copyButton, "error", "Failed", icon); + + setTimeout(() => { + resetButtonState(copyButton, originalTitle, icon); + }, ERROR_TIMEOUT); + } + } + + function handleViewMarkdown(e) { + e.preventDefault(); + closeDropdown(); + + const markdownUrl = buildMarkdownUrl(); + window.open(markdownUrl, "_blank"); + } + + function handleAIIntegration(e) { + e.preventDefault(); + closeDropdown(); + + const action = e.currentTarget.getAttribute("data-action"); + const targetUrl = buildAIServiceUrl(action); + + if (targetUrl) { + window.open(targetUrl, "_blank"); + } + } + + // Utility functions + function setButtonState(button, state, text, icon) { + button.classList.remove( + "page-copy-dropdown__item--loading", + "page-copy-dropdown__item--success", + "page-copy-dropdown__item--error" + ); + button.classList.add(`page-copy-dropdown__item--${state}`); + + const titleElement = button.querySelector( + ".page-copy-dropdown__item-title" + ); + if (titleElement) { + titleElement.textContent = text; + } + + if (icon) { + if (state === "loading") { + icon.classList.add("animate-spin"); + } else { + icon.classList.remove("animate-spin"); + } + } + + button.disabled = state === "loading"; + } + + function resetButtonState(button, originalTitle, icon) { + button.classList.remove( + "page-copy-dropdown__item--loading", + "page-copy-dropdown__item--success", + "page-copy-dropdown__item--error" + ); + + const titleElement = button.querySelector( + ".page-copy-dropdown__item-title" + ); + if (titleElement) { + titleElement.textContent = originalTitle; + } + + if (icon) { + icon.classList.remove("animate-spin"); + } + + button.disabled = false; + } + + async function fetchMarkdownContent() { + const markdownUrl = buildMarkdownUrl(); + const response = await fetch(markdownUrl); + + if (!response.ok) { + throw new Error( + `Failed to fetch markdown: ${response.status} ${response.statusText}` + ); + } + + return response.text(); + } + + async function copyToClipboard(text) { + if (!navigator.clipboard) { + throw new Error("Clipboard API not available"); + } + + await navigator.clipboard.writeText(text); + } + + function buildMarkdownUrl() { + const currentPath = window.location.pathname; + return currentPath.endsWith("/") + ? currentPath + "index.md" + : currentPath + ".md"; + } + + function buildAIServiceUrl(action) { + const markdownUrl = buildProductionMarkdownUrl(); + + switch (action) { + case "open-chatgpt": + return buildServiceUrl( + "chatgpt", + `Read and analyze this Buildkite documentation page so I can ask you questions about it: ${markdownUrl}` + ); + case "open-claude": + return buildServiceUrl( + "claude", + `Read and analyze this Buildkite documentation page so I can ask you questions about it: ${markdownUrl}` + ); + // Removed until we have have hosted MCP + // case "connect-cursor": + // return AI_SERVICES.cursor.url; + default: + return null; + } + } + + function buildServiceUrl(service, prompt) { + const config = AI_SERVICES[service]; + if (!config) return null; + + const url = new URL(config.url); + if (config.queryParam) { + url.searchParams.set(config.queryParam, prompt); + } + return url.toString(); + } + + function buildProductionMarkdownUrl() { + const currentUrl = window.location.href; + const productionUrl = currentUrl.replace( + /https?:\/\/localhost:\d+/, + PRODUCTION_BASE_URL + ); + const cleanUrl = productionUrl.replace(/\/$/, ""); + return cleanUrl + ".md"; + } + + // Return cleanup function + return function cleanup() { + abortController.abort(); + }; +} + +/** + * Safely positions the dropdown inline with the first h1 heading using a container wrapper + * instead of dangerous DOM manipulation + */ +function positionDropdownWithHeading(dropdown) { + const article = dropdown.closest(".Article"); + if (!article) return; + + const firstH1 = article.querySelector("h1"); + if (!firstH1) return; + + // Create a container wrapper to hold both h1 and dropdown + const container = document.createElement("div"); + container.className = "page-heading-container"; + + // Insert container before the h1 + firstH1.insertAdjacentElement("beforebegin", container); + + // Move h1 into container + container.appendChild(firstH1); + + // Move dropdown into container and mark as inline + dropdown.classList.add("page-copy-dropdown--inline"); + container.appendChild(dropdown); +} diff --git a/app/frontend/components/themeToggle.js b/app/frontend/components/themeToggle.js new file mode 100644 index 00000000000..8138ec19186 --- /dev/null +++ b/app/frontend/components/themeToggle.js @@ -0,0 +1,45 @@ +export function themeToggle() { + const themeSelect = document.querySelector("#theme-select"); + + function setTheme(theme) { + localStorage.setItem("docs-theme", theme); + updateAppearance(); + } + + function updateAppearance() { + let storedTheme = localStorage.getItem("docs-theme") || "system"; + let systemPrefersDark = window.matchMedia( + "(prefers-color-scheme: dark)" + ).matches; + + themeSelect.value = storedTheme; + document + .querySelectorAll(".theme-icon") + .forEach((icon) => icon.classList.add("theme-inactive")); + + if ( + storedTheme === "dark" || + (storedTheme === "system" && systemPrefersDark) + ) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + + document + .querySelector(`.theme-${storedTheme}`) + .classList.remove("theme-inactive"); + } + + themeSelect.addEventListener("change", function () { + setTheme(this.value); + }); + + window.matchMedia("(prefers-color-scheme: dark)").addListener(function () { + if (localStorage.getItem("docs-theme") === "system") { + updateAppearance(); + } + }); + + updateAppearance(); +} diff --git a/app/frontend/css/base/_article.scss b/app/frontend/css/base/_article.scss index 01c6aa9fb8b..e864cfc68c9 100644 --- a/app/frontend/css/base/_article.scss +++ b/app/frontend/css/base/_article.scss @@ -134,6 +134,7 @@ } } + p, li, dt, td, diff --git a/app/frontend/css/base/_fonts.scss b/app/frontend/css/base/_fonts.scss index 489809793f7..d8abfd1a06d 100644 --- a/app/frontend/css/base/_fonts.scss +++ b/app/frontend/css/base/_fonts.scss @@ -14,7 +14,7 @@ @font-face { font-family: Aeonik; - src: url("https://buildkite.com/_site/fonts/aeonik/Aeonik-Medium.woff2") + src: url("https://buildkite.com/_site/assets/fonts/Aeonik-Medium.woff2") format("woff2"); font-weight: 500; font-style: normal; @@ -23,27 +23,9 @@ @font-face { font-family: Aeonik; - src: url("https://buildkite.com/_site/fonts/aeonik/Aeonik-MediumItalic.woff2") - format("woff2"); - font-weight: 500; - font-style: italic; - font-display: swap; -} - -@font-face { - font-family: Aeonik; - src: url("https://buildkite.com/_site/fonts/aeonik/Aeonik-Bold.woff2") + src: url("https://buildkite.com/_site/assets/fonts/Aeonik-Bold.woff2") format("woff2"); font-weight: 700; font-style: normal; font-display: swap; } - -@font-face { - font-family: Aeonik; - src: url("https://buildkite.com/_site/fonts/aeonik/Aeonik-BoldItalic.woff2") - format("woff2"); - font-weight: 700; - font-style: italic; - font-display: swap; -} diff --git a/app/frontend/css/base/_variables.scss b/app/frontend/css/base/_variables.scss index 0203c1f6a51..2121b3712d8 100644 --- a/app/frontend/css/base/_variables.scss +++ b/app/frontend/css/base/_variables.scss @@ -37,6 +37,7 @@ $navy-600: #4f4a6a; $navy-700: #383451; $navy-800: #28243f; $navy-900: #140d30; +$navy-950: #0e0923; $gray-100: #f8f8f8; $gray-200: #f5f5f5; $gray-300: #eeeeee; diff --git a/app/frontend/css/components/_copy_to_clipboard_button.scss b/app/frontend/css/components/_copy_to_clipboard_button.scss index df5ae444b2d..af756d82bcf 100644 --- a/app/frontend/css/components/_copy_to_clipboard_button.scss +++ b/app/frontend/css/components/_copy_to_clipboard_button.scss @@ -19,6 +19,7 @@ padding: 0; border: 0; + color: #0f172a; } // Render when focusing sibling (
) element
diff --git a/app/frontend/css/components/_frame_header.scss b/app/frontend/css/components/_frame_header.scss
new file mode 100644
index 00000000000..2a9ca551b51
--- /dev/null
+++ b/app/frontend/css/components/_frame_header.scss
@@ -0,0 +1,40 @@
+.Frameheader {
+  border-radius: 8px 8px 0 0;
+  display: block;
+  text-align: center;
+  padding: 4px 8px;
+  border: 1px solid #ddd;
+  border-bottom: 0 none;
+  width: 100%;
+  box-sizing: border-box;
+  font-size: 14px !important;
+}
+
+.Frameheader__address {
+  display: inline-block;
+  padding: 1px 12px;
+  background: $navy-100;
+  color: $navy-600;
+  border-radius: 6px;
+  min-width: 50%;
+  transition: color 0.2s ease;
+}
+
+.Frameheader__address::after {
+  content: "→";
+  padding-left: 4px;
+  display: inline-block;
+  transform: translateX(-50%);
+  opacity: 0;
+  transition: transform 0.2s ease, opacity 0.2s ease;
+}
+
+.Frameheader:hover {
+  .Frameheader__address {
+    color: $purple1-600;
+  }
+  .Frameheader__address::after {
+    transform: translateX(0%);
+    opacity: 1;
+  }
+}
diff --git a/app/frontend/css/components/_global_links.scss b/app/frontend/css/components/_global_links.scss
index f1ce77fe0bd..b065eb042b6 100644
--- a/app/frontend/css/components/_global_links.scss
+++ b/app/frontend/css/components/_global_links.scss
@@ -1,10 +1,12 @@
 .GlobalLinks {
   display: flex;
-  flex-direction: column;
+  flex-direction: row;
   width: 100%;
+  align-items: center;
+  justify-content: center;
 
-  @media (min-width: $screen-sm) {
-    flex-direction: row;
+  @media (min-width: $screen-md) {
+    justify-content: end;
   }
 }
 
diff --git a/app/frontend/css/components/_index.scss b/app/frontend/css/components/_index.scss
index 1ac1200a8ba..8be1d8c0fa6 100644
--- a/app/frontend/css/components/_index.scss
+++ b/app/frontend/css/components/_index.scss
@@ -6,8 +6,10 @@
 @import "cards_grid";
 @import "copy_to_clipboard_button";
 @import "divider";
+@import "page_copy_dropdown";
 @import "dropdown";
 @import "footer";
+@import "frame_header";
 @import "global_links";
 @import "heading_group";
 @import "hero";
@@ -20,4 +22,5 @@
 @import "site_header";
 @import "status_badges";
 @import "tiles";
+@import "theme_select";
 @import "toc";
diff --git a/app/frontend/css/components/_menu_toggle.scss b/app/frontend/css/components/_menu_toggle.scss
index 33df73b647c..4cfc804ec7f 100644
--- a/app/frontend/css/components/_menu_toggle.scss
+++ b/app/frontend/css/components/_menu_toggle.scss
@@ -14,6 +14,9 @@
   &:checked ~ .SiteHeader__nav,
   &:checked ~ .SiteHeader__global-links {
     display: flex;
+    @media (max-width: 959px) {
+      width: 100%;
+    }
   }
 }
 
diff --git a/app/frontend/css/components/_nav.scss b/app/frontend/css/components/_nav.scss
index cce6689065e..9c5dd6e6176 100644
--- a/app/frontend/css/components/_nav.scss
+++ b/app/frontend/css/components/_nav.scss
@@ -47,6 +47,7 @@
 
   &__link {
     align-items: center;
+    text-align: left;
     background-color: transparent;
     border: none;
     border-radius: 4px;
@@ -130,6 +131,46 @@
       }
     }
 
+    &--level5 {
+      &::before {
+        left: 62px;
+
+        @media (min-width: $screen-md) {
+          left: 42px;
+        }
+      }
+    }
+
+    &--level6 {
+      &::before {
+        left: 72px;
+
+        @media (min-width: $screen-md) {
+          left: 52px;
+        }
+      }
+    }
+
+    &--level7 {
+      &::before {
+        left: 82px;
+
+        @media (min-width: $screen-md) {
+          left: 62px;
+        }
+      }
+    }
+
+    &--level8 {
+      &::before {
+        left: 92px;
+
+        @media (min-width: $screen-md) {
+          left: 72px;
+        }
+      }
+    }
+
     &--type {
       &-back {
         &::before {
@@ -201,6 +242,30 @@
         padding-left: 55px;
       }
     }
+
+    &--level6 {
+      padding-left: 90px;
+
+      @media (min-width: $screen-md) {
+        padding-left: 66px;
+      }
+    }
+
+    &--level7 {
+      padding-left: 100px;
+
+      @media (min-width: $screen-md) {
+        padding-left: 77px;
+      }
+    }
+
+    &--level8 {
+      padding-left: 110px;
+
+      @media (min-width: $screen-md) {
+        padding-left: 88px;
+      }
+    }
   }
 
   &__toggle {
diff --git a/app/frontend/css/components/_page.scss b/app/frontend/css/components/_page.scss
index aea46f5671c..380964417b5 100644
--- a/app/frontend/css/components/_page.scss
+++ b/app/frontend/css/components/_page.scss
@@ -10,7 +10,7 @@ $lg-screen-margin-top: map-get($header-height, "one-row");
 
 .Page {
   display: grid;
-  grid-template-rows: auto;
+  grid-template-rows: minmax(0, auto);
   grid-template-columns: 1fr;
   grid-template-areas:
     "sidebar"
diff --git a/app/frontend/css/components/_page_copy_dropdown.scss b/app/frontend/css/components/_page_copy_dropdown.scss
new file mode 100644
index 00000000000..1be1052ad77
--- /dev/null
+++ b/app/frontend/css/components/_page_copy_dropdown.scss
@@ -0,0 +1,312 @@
+@import "../base/variables";
+
+.PageHeader {
+  display: flex;
+  justify-content: flex-end;
+  margin-bottom: 24px;
+  padding: 0;
+
+  @media (max-width: 768px) {
+    margin-bottom: 16px;
+  }
+}
+
+.Article[data-has-copy-dropdown="true"] {
+  .page-heading-container {
+    display: flex;
+    align-items: flex-start;
+    gap: 1rem;
+    margin-top: 0;
+    margin-bottom: 1.5rem;
+    line-height: 1.3;
+
+    @media (max-width: 768px) {
+      flex-wrap: nowrap;
+      gap: 0.5rem;
+    }
+
+    h1 {
+      flex: 1;
+      margin: 0;
+      min-width: 0;
+      word-wrap: break-word;
+      hyphens: auto;
+    }
+
+    .page-copy-dropdown--inline {
+      flex-shrink: 0;
+      margin: 0;
+      align-self: flex-start;
+    }
+  }
+}
+
+.page-copy-dropdown {
+  position: relative;
+  display: inline-flex;
+  margin-bottom: 1rem;
+  flex-shrink: 0;
+
+  &.page-copy-dropdown--inline {
+    margin-bottom: 0;
+    order: 1;
+
+    @media (max-width: 768px) {
+      // order: -1;
+      // margin-bottom: 0.5rem;
+      // align-self: flex-start;
+    }
+  }
+
+  &__button {
+    @extend .Button;
+    @extend .Button--default;
+    @extend .Button--small;
+
+    --icon-color: #{rgba(map-get($color-aliases, "icon"), 0.8)};
+
+    display: flex;
+    align-items: center;
+    padding-left: 12px;
+    gap: 8px;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    background: $base-0;
+    color: map-get($color-aliases, "text-base");
+    white-space: nowrap;
+    transition: all 0.15s ease;
+
+    &:hover {
+      border-color: map-get($color-aliases, "border");
+      transform: translateY(-1px);
+    }
+
+    &:active {
+      transform: translateY(0);
+    }
+
+    svg {
+      flex-shrink: 0;
+    }
+
+    .lucide-copy {
+      color: rgba(map-get($color-aliases, "icon"), 0.8);
+    }
+
+    .lucide-chevron-down {
+      margin-left: 5px;
+
+      @media (max-width: 768px) {
+        margin-left: 2px;
+      }
+    }
+
+    @media (max-width: 768px) {
+      padding: 8px 10px;
+      gap: 4px;
+      min-width: auto;
+    }
+
+    @media (max-width: 480px) {
+      padding: 6px 5px 6px 8px;
+      gap: 2px;
+    }
+  }
+
+  &__button-text-full {
+    @media (max-width: 768px) {
+      display: none;
+    }
+  }
+
+  &__button-text-short {
+    display: none;
+
+    @media (max-width: 768px) {
+      display: none;
+    }
+  }
+
+  &__menu {
+    position: absolute;
+    top: 100%;
+    right: 0;
+    margin-top: 4px;
+    padding: 4px;
+    background: $base-0;
+    border: 1px solid map-get($color-aliases, "border");
+    border-radius: 6px;
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+      0 4px 6px -2px rgba(0, 0, 0, 0.05);
+    min-width: 260px;
+    z-index: 50;
+    opacity: 0;
+    visibility: hidden;
+    transform: translateY(-8px);
+    transition: all 0.15s ease;
+
+    &--open {
+      opacity: 1;
+      visibility: visible;
+      transform: translateY(0);
+    }
+
+    @media (max-width: 768px) {
+      max-width: calc(100vw - 16px);
+      box-shadow: 0 8px 12px -2px rgba(0, 0, 0, 0.1),
+        0 2px 4px -1px rgba(0, 0, 0, 0.05);
+    }
+
+    @media (max-width: 480px) {
+      max-width: calc(100vw - 32px);
+    }
+  }
+
+  &__divider {
+    height: 1px;
+    background: $gray-100;
+    margin: 4px 0;
+  }
+
+  &__item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    width: 100%;
+    padding: 12px 12px;
+    border: none;
+    border-radius: 4px;
+    background: none;
+    color: map-get($color-aliases, "text-base");
+    font-family: $font-family-base;
+    font-size: 14px;
+    font-weight: 500;
+    text-align: left;
+    cursor: pointer;
+    transition: all 0.15s ease;
+    text-decoration: none;
+
+    &:hover {
+      background: map-get($color-aliases, "panel-background");
+      color: $navy-900;
+    }
+
+    &:first-child {
+      border-top-left-radius: 5px;
+      border-top-right-radius: 5px;
+    }
+
+    &:last-child {
+      border-bottom-left-radius: 5px;
+      border-bottom-right-radius: 5px;
+    }
+
+    svg {
+      flex-shrink: 0;
+      color: map-get($color-aliases, "icon");
+    }
+
+    @media (max-width: 768px) {
+      padding: 10px 8px;
+      gap: 10px;
+      font-size: 13px;
+    }
+
+    @media (max-width: 480px) {
+      padding: 8px 6px;
+      gap: 8px;
+    }
+  }
+
+  &__item {
+    &--loading {
+      color: map-get($color-aliases, "icon");
+      pointer-events: none;
+
+      .page-copy-dropdown__item-icon svg {
+        color: map-get($color-aliases, "icon");
+      }
+    }
+
+    &--success {
+      color: map-get($color-aliases, "state-success");
+
+      .page-copy-dropdown__item-icon svg {
+        color: map-get($color-aliases, "state-success");
+      }
+    }
+
+    &--error {
+      color: map-get($color-aliases, "state-fail");
+
+      .page-copy-dropdown__item-icon svg {
+        color: map-get($color-aliases, "state-fail");
+      }
+    }
+  }
+
+  &__item-icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 20px;
+    height: 20px;
+    flex-shrink: 0;
+
+    svg {
+      color: map-get($color-aliases, "icon");
+    }
+  }
+
+  &__item-content {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+  }
+
+  &__item-title {
+    font-weight: 500;
+    font-size: 14px;
+    line-height: 1.2;
+  }
+
+  &__item-subtitle {
+    font-size: 12px;
+    color: $charcoal-300;
+    line-height: 1.2;
+  }
+
+  &__item-external {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 16px;
+    height: 16px;
+    flex-shrink: 0;
+    opacity: 0.6;
+
+    svg {
+      color: currentColor;
+    }
+  }
+
+  // Temporarily hide Cursor MCP connection until hosted server is ready
+  &__item[data-action="connect-cursor"] {
+    display: none;
+  }
+}
+
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.animate-spin {
+  animation: spin 1s linear infinite;
+}
diff --git a/app/frontend/css/components/_responsive_table.scss b/app/frontend/css/components/_responsive_table.scss
index 18342dac62a..9ffc94d15fa 100644
--- a/app/frontend/css/components/_responsive_table.scss
+++ b/app/frontend/css/components/_responsive_table.scss
@@ -103,6 +103,15 @@
     }
   }
 
+  code {
+    /* Including this seems to do the opposite of what's intended. */
+    /* overflow-wrap: break-word; */
+    display: inline-block;
+    /* Forces internal wrapping of longer code lines, making them look ugly in table cells. */
+    /* max-width: 30ch; */
+    vertical-align: top;
+  }
+
   &--single-column-rows {
     tbody {
       tr {
@@ -128,4 +137,13 @@
       display: none;
     }
   }
+
+  &--wrap-th-codeblocks tbody {
+    th code,
+    td:first-child code {
+      @media (min-width: $screen-lg) {
+        white-space: wrap;
+      }
+    }
+  }
 }
diff --git a/app/frontend/css/components/_site_header.scss b/app/frontend/css/components/_site_header.scss
index a5de22878c1..3a5bc122e00 100644
--- a/app/frontend/css/components/_site_header.scss
+++ b/app/frontend/css/components/_site_header.scss
@@ -109,3 +109,11 @@
     color: map-get($color-aliases, "brand");
   }
 }
+
+.Wordmark__Brand {
+  color: #1a002e;
+}
+
+.Wordmark__SubBrand {
+  color: #504581;
+}
diff --git a/app/frontend/css/components/_theme_select.scss b/app/frontend/css/components/_theme_select.scss
new file mode 100644
index 00000000000..b42f0ce86ca
--- /dev/null
+++ b/app/frontend/css/components/_theme_select.scss
@@ -0,0 +1,44 @@
+.theme-ui {
+  border-radius: 6px;
+  border: 1px solid $navy-200;
+  display: block;
+  position: relative;
+  svg {
+    color: $navy-600;
+  }
+}
+
+.theme-chevron {
+  width: 16px;
+  height: 16px;
+  position: absolute;
+  right: 6px;
+  top: 10px;
+  pointer-events: none;
+}
+
+.theme-icon {
+  width: 20px;
+  position: absolute;
+  top: 8px;
+  left: 8px;
+  pointer-events: none;
+}
+
+.theme-inactive {
+  display: none;
+}
+
+#theme-select {
+  appearance: none;
+  font-size: 14px;
+  margin-left: 12px;
+  line-height: 16px;
+  border: 0 none;
+  background: transparent;
+  padding: 10px 24px 10px 20px;
+  color: currentColor;
+  &:focus {
+    outline: none;
+  }
+}
diff --git a/app/frontend/css/components/_tiles.scss b/app/frontend/css/components/_tiles.scss
index 1f04b32bd46..9185ff6fe26 100644
--- a/app/frontend/css/components/_tiles.scss
+++ b/app/frontend/css/components/_tiles.scss
@@ -20,7 +20,7 @@
   }
 
   .TileItem__title {
-    font-size: 1.1875rem;
+    font-size: 1rem;
     font-weight: 500;
     margin: 0 0 8px;
   }
diff --git a/app/frontend/css/pages/_docs.scss b/app/frontend/css/pages/_docs.scss
index 65983ed1b09..bbfaa0b9cc0 100644
--- a/app/frontend/css/pages/_docs.scss
+++ b/app/frontend/css/pages/_docs.scss
@@ -37,7 +37,7 @@
     transition: all 0.2s;
   }
 
-  &:hover {
+  th:hover {
     .Docs__attribute__link {
       opacity: 1;
 
@@ -88,21 +88,20 @@
 }
 
 a.Docs__example-repo {
-  border: 1px solid #ccc;
   color: map-get($color-aliases, "text-base");
   display: flex;
-  padding: 15px;
+  padding: 12px 16px;
   text-decoration: none;
+  border-radius: $rounded;
+  background: $base-0;
+  box-shadow: $box-shadow-depth-100;
+  transition: box-shadow 0.2s ease;
 
   &:hover,
   &:active,
   &:focus {
-    border-color: map-get($color-aliases, "brand");
     color: black;
-
-    .repo {
-      color: map-get($color-aliases, "brand");
-    }
+    box-shadow: $box-shadow-depth-100-hover;
   }
 
   .icon {
diff --git a/app/frontend/css/pages/homepage/_products.scss b/app/frontend/css/pages/homepage/_products.scss
index cc509aed9fa..c6aa9bce150 100644
--- a/app/frontend/css/pages/homepage/_products.scss
+++ b/app/frontend/css/pages/homepage/_products.scss
@@ -6,12 +6,14 @@
 .Products__row {
   display: flex;
   flex-direction: column;
-  gap: 24px;
+  gap: 12px;
   margin-left: $gutter / -2;
   margin-right: $gutter / -2;
 
   @media (min-width: $screen-sm) {
     flex-direction: row;
+    align-items: stretch;
+    gap: 24px;
   }
 
   @media (min-width: $screen-md) {
@@ -22,9 +24,11 @@
 
 .Products__link {
   text-decoration: none;
+  flex: 1;
 }
 
 .ProductCard {
+  box-sizing: border-box;
   background-color: $base-0;
   border-radius: $rounded;
   box-shadow: $box-shadow-depth-100;
@@ -33,6 +37,7 @@
   gap: $gutter / 2;
   padding: $gutter / 2;
   transition: box-shadow $transition-speed;
+  height: 100%;
 
   @media (min-width: $screen-xs) {
     flex-direction: row;
@@ -55,6 +60,29 @@
 
 .ProductCard__img {
   @media (min-width: $screen-xs) {
-    width: 148px;
+    height: 50px;
+  }
+}
+
+.ProductBadge {
+  display: inline-flex;
+  align-items: center;
+  border-radius: 12px;
+  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1),
+    0px 0px 0px 1px rgba(0, 0, 0, 0.15), 0px 1px 0px 0px rgba(0, 0, 0, 0.09),
+    0px 2px 2px 0px rgba(0, 0, 0, 0.04), 0px 3px 3px 0px rgba(0, 0, 0, 0.02),
+    0px 4px 4px 0px rgba(0, 0, 0, 0.01);
+  padding-right: 12px;
+  background: #fff;
+  font-size: 15px;
+  color: #28243f;
+  font-weight: 600;
+  margin-bottom: 16px;
+  svg {
+    width: 24px;
+    height: 24px;
+    padding: 8px;
+    border-right: 1px solid rgba(0, 0, 0, 0.2);
+    margin-right: 12px;
   }
 }
diff --git a/app/frontend/css/utilities/_dark_mode.scss b/app/frontend/css/utilities/_dark_mode.scss
new file mode 100644
index 00000000000..3801c207a7d
--- /dev/null
+++ b/app/frontend/css/utilities/_dark_mode.scss
@@ -0,0 +1,475 @@
+.dark-only {
+  display: none;
+}
+
+html.dark {
+  background: #020617;
+
+  --docsearch-text-color: rgb(245, 246, 247);
+  --docsearch-container-background: rgba(9, 10, 17, 0.8);
+  --docsearch-modal-background: rgb(21, 23, 42);
+  --docsearch-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64),
+    0 3px 8px 0 rgb(0, 3, 9);
+  --docsearch-searchbox-background: rgb(9, 10, 17);
+  --docsearch-searchbox-focus-background: #000;
+  --docsearch-hit-color: rgb(190, 195, 201);
+  --docsearch-hit-shadow: none;
+  --docsearch-hit-background: rgb(9, 10, 17);
+  --docsearch-key-gradient: linear-gradient(
+    -26.5deg,
+    rgb(86, 88, 114) 0%,
+    rgb(49, 53, 91) 100%
+  );
+  --docsearch-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85),
+    inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, 0.3);
+  --docsearch-footer-background: rgb(30, 33, 54);
+  --docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),
+    0 -4px 8px 0 rgba(0, 0, 0, 0.2);
+  --docsearch-muted-color: rgb(127, 132, 151);
+  --docsearch-logo-color: rgb(255, 255, 255);
+
+  .SiteHeader,
+  .Page__sidebar,
+  .SiteHeader__nav,
+  .divider,
+  .Article tbody tr,
+  iframe,
+  hr {
+    border-color: $navy-800;
+  }
+
+  .Article thead {
+    border-color: $navy-700;
+  }
+
+  .Wordmark__Brand {
+    color: #ffffff;
+  }
+
+  .Wordmark__SubBrand {
+    color: $navy-400;
+  }
+
+  .SiteHeader,
+  .Nav--sidebar .Nav__section--level2 {
+    background: #020617;
+  }
+
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6,
+  .TileItem .TileItem__title-link,
+  .Docs__heading .Docs__heading__anchor,
+  .Footer__heading,
+  .Card__title-link {
+    color: $navy-50;
+  }
+
+  .Nav__link,
+  .Dropdown__menu-link {
+    color: $navy-300;
+    &:hover {
+      background: #1f023e;
+      color: $navy-200;
+    }
+  }
+
+  .Toc__link {
+    color: $navy-300;
+  }
+
+  .Nav__link--parent {
+    color: #e5e1ff !important;
+  }
+
+  .Nav__link--current {
+    background: #340469 !important;
+    color: #e5e1ff !important;
+  }
+
+  .Article,
+  .Footer,
+  .HeroBanner__summary,
+  .ProductCard__summary,
+  .Card__list-item-link,
+  .Reference__link,
+  p {
+    color: $navy-300;
+    a {
+      color: #9f83fe;
+      text-decoration-color: #340469;
+      &:hover {
+        text-decoration-color: #9f83fe;
+      }
+    }
+  }
+
+  .Article pre {
+    border-color: #1e293b;
+    background-color: #0f172a;
+  }
+
+  .Article .highlight-figure figcaption {
+    border-color: #1e293b;
+    color: $navy-500;
+    background-color: #0f172a;
+  }
+
+  .highlight {
+    color: $navy-300;
+  }
+
+  .CopyToClipboardButton {
+    background: $navy-600 !important;
+    svg {
+      color: $navy-200 !important;
+    }
+  }
+
+  .highlight .s,
+  .highlight .sb,
+  .highlight .sc,
+  .highlight .s2,
+  .highlight .se,
+  .highlight .sh,
+  .highlight .si,
+  .highlight .sx,
+  .highlight .sr,
+  .highlight .s1,
+  .highlight .ss {
+    color: #00e285;
+  }
+
+  .highlight .na,
+  .highlight .nc,
+  .highlight .nd,
+  .highlight .ni,
+  .highlight .ne,
+  .highlight .nf,
+  .highlight .nl,
+  .highlight .nn,
+  .highlight .nx,
+  .highlight .py,
+  .highlight .nv,
+  .highlight .vc,
+  .highlight .vg,
+  .highlight .vi {
+    color: #9f83fe;
+  }
+
+  .highlight .k,
+  .highlight .kc,
+  .highlight .kd,
+  .highlight .kn,
+  .highlight .kp,
+  .highlight .kr,
+  .highlight .kt,
+  .highlight .nb,
+  .highlight .bp,
+  .highlight .nt,
+  .highlight .no,
+  .highlight .o,
+  .highlight .ow,
+  .highlight .mf,
+  .highlight .mh,
+  .highlight .mi,
+  .highlight .il,
+  .highlight .mo,
+  .highlight .ld,
+  .highlight .m {
+    color: #f2545b;
+  }
+
+  .callout {
+    background: #0f172a;
+    color: $navy-300;
+    p {
+      color: inherit;
+    }
+  }
+
+  .Button--default,
+  .Article details {
+    background-color: #0f172a;
+    color: $navy-50 !important;
+    box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.01),
+      0px 3px 3px 0px rgba(0, 0, 0, 0.02), 0px 2px 2px 0px rgba(0, 0, 0, 0.04),
+      0px 1px 0px 0px rgba(0, 0, 0, 0.09), 0px 0px 0px 1px rgba(0, 0, 0, 0.09);
+  }
+
+  .Button--default:hover,
+  .PopularGuide:hover {
+    background-color: #1e293b;
+  }
+
+  .Article table {
+    box-shadow: #121e3c 0px 0px 0px 1px, #10172ac7 0px 1px 3px;
+  }
+
+  .Article h1:not([class]) > code,
+  .Article h2:not([class]) > code,
+  .Article h3:not([class]) > code,
+  .Article h4:not([class]) > code,
+  .Article h5:not([class]) > code,
+  .Article h6:not([class]) > code,
+  .Article p:not([class]) > code,
+  .Article a:not([class]) > code,
+  .Article p > code,
+  .Article li > code,
+  .Article dt > code,
+  .Article td > code,
+  .Article th > code {
+    background-color: #0f172a;
+  }
+
+  .callout p code {
+    background-color: #ffffff0f !important;
+  }
+
+  .theme-ui {
+    border-color: #1e293b;
+    svg {
+      color: $navy-400;
+    }
+  }
+
+  #theme-select {
+    color: $navy-300;
+  }
+
+  .ProductCard,
+  .Card {
+    background: #0f172a;
+  }
+
+  .Card__title-link:hover,
+  .Card__icon-link:hover + .Card__title .Card__title-link {
+    color: #e5e1ff;
+  }
+
+  .Toc .Toc__list-item.active > .Toc__link,
+  .Toc .Toc__link:hover {
+    color: #9f83fe;
+  }
+
+  .Toc path {
+    stroke: #9f83fe;
+  }
+
+  .DocSearch-Button {
+    color: #fff;
+    background: #020617;
+  }
+
+  .Toc {
+    background: #020617;
+    border: 1px solid $navy-800;
+    @media (min-width: 1280px) {
+      border: 0 none;
+      background: transparent;
+    }
+  }
+
+  .Dropdown,
+  .Dropdown__menu {
+    background: #020617;
+    color: #fff;
+  }
+
+  .Dropdown__menu {
+    border: 1px solid $navy-800;
+  }
+
+  .Dropdown__menu::before {
+    border-bottom-color: $navy-800;
+  }
+
+  .Dropdown__menu::after {
+    border-bottom-color: #020617;
+  }
+
+  .Button {
+    color: $navy-50;
+  }
+
+  a.Docs__example-repo {
+    background: #020617;
+    border-color: $navy-700;
+    color: $navy-300;
+    box-shadow: 0px 4px 4px 0px rgba($base-1000, 0.01),
+      0px 3px 3px 0px rgba($base-1000, 0.18),
+      0px 2px 2px 0px rgba($base-1000, 0.08),
+      0px 1px 0px 0px rgba($base-1000, 0.18),
+      0px 0px 0px 1px rgba($base-1000, 0.18),
+      inset 0 1px 0 rgba(255, 255, 255, 0.075),
+      inset 0 0 0 1px rgba(255, 255, 255, 0.09);
+
+    &:hover,
+    &:active,
+    &:focus {
+      box-shadow: 0px 4px 4px 0px rgba($base-1000, 0.01),
+        0px 3px 3px 0px rgba($base-1000, 0.18),
+        0px 2px 2px 0px rgba($base-1000, 0.08),
+        0px 1px 0px 0px rgba($base-1000, 0.18),
+        0px 0px 0px 1px rgba($base-1000, 0.18),
+        inset 0 1px 0 rgba(255, 255, 255, 0.1),
+        inset 0 0 0 1px rgba(255, 255, 255, 0.1);
+    }
+    .repo {
+      color: $navy-400;
+      &:hover {
+        color: $purple1-500;
+      }
+    }
+  }
+
+  .callout {
+    @include callout($navy-200, #0f172a, default_dark);
+    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1) !important;
+    .callout__anchor {
+      &:hover,
+      &:active,
+      &:focus {
+        color: $navy-50 !important;
+      }
+    }
+  }
+
+  .callout--troubleshooting {
+    @include callout(#fed7aa, #431407, troubleshooting_dark);
+    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1) !important;
+    .callout__anchor {
+      &:hover,
+      &:active,
+      &:focus {
+        color: $navy-50 !important;
+      }
+    }
+  }
+
+  .callout--wip {
+    @include callout(#d6d3d1, #1c1917, wip_dark);
+    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1) !important;
+    .callout__anchor {
+      &:hover,
+      &:active,
+      &:focus {
+        color: $navy-50 !important;
+      }
+    }
+  }
+
+  .Button__icon--16x16,
+  .MenuToggleLabel {
+    filter: invert(92%) hue-rotate(180deg);
+  }
+
+  .MenuToggleLabel {
+    background-color: #fff;
+    &:hover {
+      background-color: #eee;
+    }
+  }
+
+  .ProductBadge {
+    background: #1e293b;
+    color: #fff;
+    svg {
+      border-right: 1px solid rgba(255, 255, 255, 0.2);
+      margin-right: 8px;
+    }
+  }
+
+  .invertible {
+    filter: invert(92%) hue-rotate(180deg);
+  }
+
+  .Frameheader {
+    border-color: $navy-800;
+  }
+
+  .Frameheader__address {
+    background: $navy-900;
+    color: $navy-400;
+  }
+
+  .Frameheader:hover .Frameheader__address {
+    color: $purple1-400;
+  }
+
+  .light-only {
+    display: none;
+  }
+
+  .dark-only {
+    display: block;
+  }
+
+  .responsive-image-container {
+    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2),
+      0 -1px 0 0 rgba(255, 255, 255, 0.05);
+  }
+
+  .page-copy-dropdown {
+    &__button {
+      --icon-color: $navy-400;
+      border-color: #1e293b;
+
+      &:hover {
+        border-color: rgba(map-get($color-aliases, "icon"), 0.6);
+        transform: translateY(-1px);
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
+      }
+
+      &:active {
+        transform: translateY(0);
+        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+      }
+
+      .lucide-copy {
+        color: $navy-100;
+      }
+    }
+
+    &__menu {
+      background: #020617;
+      border-color: $navy-800;
+    }
+
+    &__item {
+      color: $gray-100;
+
+      &:hover {
+        background: #1f023e;
+        color: $navy-200;
+      }
+
+      &--loading {
+        color: $gray-500;
+
+        .page-copy-dropdown__item-icon svg {
+          color: $gray-500;
+        }
+      }
+    }
+
+    &__item-icon svg {
+      color: $gray-500;
+    }
+
+    &__item-subtitle {
+      color: $gray-500;
+    }
+
+    &__divider {
+      background: #140d30;
+    }
+
+    &__item[data-action="open-chatgpt"] svg path {
+      fill: white !important;
+    }
+  }
+}
diff --git a/app/frontend/css/utilities/_index.scss b/app/frontend/css/utilities/_index.scss
index a6d29b0f88e..e3021a68700 100644
--- a/app/frontend/css/utilities/_index.scss
+++ b/app/frontend/css/utilities/_index.scss
@@ -6,3 +6,4 @@
 @import "responsive_image_container";
 @import "sr_only";
 @import "standard_top_section";
+@import "dark_mode";
diff --git a/app/frontend/entrypoints/application.js b/app/frontend/entrypoints/application.js
index 1a201db67ef..a154e14a932 100644
--- a/app/frontend/entrypoints/application.js
+++ b/app/frontend/entrypoints/application.js
@@ -1,12 +1,18 @@
 import * as Turbo from "@hotwired/turbo";
-import { bindToggles } from "../components/nav";
+import { bindToggles, preserveScroll } from "../components/nav";
 import { initToc } from "../components/toc";
 import { attachCopyToClipboardButton } from "../components/copyToClipboardButton";
+import { themeToggle } from "../components/themeToggle";
+import { initPageCopyDropdown } from "../components/pageCopyDropdown";
 import docsearch from "@docsearch/js";
 
 Turbo.start();
 
+// Store cleanup functions for proper memory management
+let cleanupFunctions = [];
+
 document.addEventListener("turbo:render", async (event) => {
+  cleanup();
   render();
 });
 
@@ -14,6 +20,20 @@ window.addEventListener("DOMContentLoaded", () => {
   render();
 });
 
+function cleanup() {
+  // Call all cleanup functions from previous renders
+  cleanupFunctions.forEach((fn) => {
+    if (typeof fn === "function") {
+      try {
+        fn();
+      } catch (error) {
+        console.warn("Error during cleanup:", error);
+      }
+    }
+  });
+  cleanupFunctions = [];
+}
+
 function render() {
   docsearch({
     container: "#search",
@@ -23,8 +43,16 @@ function render() {
   });
 
   bindToggles();
+  preserveScroll();
   initToc();
   attachCopyToClipboardButton("pre.highlight");
+  themeToggle();
+
+  // Store cleanup function if component returns one
+  const dropdownCleanup = initPageCopyDropdown();
+  if (dropdownCleanup) {
+    cleanupFunctions.push(dropdownCleanup);
+  }
 }
 
 window.addEventListener("DOMContentLoaded", () => {
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0121923d9ce..fa71c2ff0eb 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -3,6 +3,10 @@ def dashboard_path
     "/dashboard"
   end
 
+  def buildkite_url
+    @buildkite_url ||= Page::BuildkiteUrl.new
+  end
+
   def nav_path(path)
     if path =~ URI::regexp
       path
diff --git a/app/models/agent_installers.rb b/app/models/agent_installers.rb
index a13779132ba..b33d90c76b7 100644
--- a/app/models/agent_installers.rb
+++ b/app/models/agent_installers.rb
@@ -1,24 +1,26 @@
-AGENT_INSTALLERS = [
+AgentInstallers = [
   { title: "Ubuntu",
-    url: "agent/v3/ubuntu" },
+    url: "agent/v3/self-hosted/install/ubuntu" },
   { title: "Debian",
-    url: "agent/v3/debian" },
+    url: "agent/v3/self-hosted/install/debian" },
   { title: "Red Hat/CentOS",
-    url: "agent/v3/redhat" },
+    url: "agent/v3/self-hosted/install/redhat" },
   { title: "FreeBSD",
-    url: "agent/v3/freebsd" },
+    url: "agent/v3/self-hosted/install/freebsd" },
   { title: "macOS",
-    url: "agent/v3/macos" },
+    url: "agent/v3/self-hosted/install/macos" },
   { title: "Windows",
-    url: "agent/v3/windows" },
+    url: "agent/v3/self-hosted/install/windows" },
   { title: "Linux",
-    url: "agent/v3/linux" },
+    url: "agent/v3/self-hosted/install/linux" },
   { title: "Docker",
-    url: "agent/v3/docker" },
+    url: "agent/v3/self-hosted/install/docker" },
+  { title: "Agent Stack for Kubernetes",
+    url: "agent/v3/self-hosted/agent-stack-k8s" },
   { title: "AWS",
-    url: "agent/v3/aws" },
+    url: "agent/v3/self-hosted/aws" },
   { title: "Elastic CI Stack for AWS",
-    url: "agent/v3/elastic-ci-aws" },
+    url: "agent/v3/self-hosted/aws/elastic-ci-stack" },
   { title: "Google Cloud",
-    url: "agent/v3/gcloud" }
+    url: "agent/v3/self-hosted/gcp" }
 ].freeze
diff --git a/app/models/beta_pages.rb b/app/models/beta_pages.rb
index 67eb97cc0ac..952c7cc0bda 100644
--- a/app/models/beta_pages.rb
+++ b/app/models/beta_pages.rb
@@ -1,14 +1,8 @@
 class BetaPages
   def self.all
     [
-      'pipelines/cluster-queue-metrics',
-      'test-analytics/test-ownership',
-      'apis/rest-api/team-pipelines',
-      'apis/rest-api/organizations/members',
-      'apis/rest-api/teams',
-      'apis/rest-api/teams/members',
-      'apis/rest-api/teams/pipelines',
-      'apis/rest-api/teams/suites'
+      'path/page-name',
+      'path/last-page-name-in-list-without-trailing-comma'
     ]
   end
 end
diff --git a/app/models/llm_text.rb b/app/models/llm_text.rb
new file mode 100644
index 00000000000..52f5ed58555
--- /dev/null
+++ b/app/models/llm_text.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+class LLMText
+  attr_reader :nav
+
+  def initialize(nav)
+    @nav = nav
+  end
+
+  class << self
+    def generate
+      new(Rails.application.config.default_nav).generate
+    end
+  end
+
+  def generate
+    content = [
+      "# Buildkite Documentation",
+      "",
+      "> Buildkite is a platform for running fast, secure, and scalable continuous integration pipelines on your own infrastructure.",
+      ""
+    ]
+
+    # Process each top-level navigation section
+    nav.data.each do |section|
+      next unless section["children"] # Skip sections without children
+
+      # Check if this section has any valid children after filtering
+      temp_content = []
+      process_nav_children(section["children"], temp_content, 3)
+
+      # Only add the section if there's actual content
+      if temp_content.any? { |line| line.start_with?("- ") || line.start_with?("#") }
+        content << "## #{section['name']}"
+        content << ""
+
+        content.concat(temp_content)
+        content << ""
+      end
+    end
+
+    content.join("\n")
+  end
+
+  private
+
+  def process_nav_children(children, content, heading_level)
+    children.each_with_index do |child, index|
+      next if child["type"] == "divider"
+      next if should_skip_item?(child)
+
+      if child["path"]
+        # This is a leaf node with a path - add it as a link
+        url = "https://buildkite.com/docs/#{child['path']}.md"
+        content << "- [#{child['name']}](#{url})"
+      elsif child["children"]
+        # Check if this section has any valid children after filtering
+        temp_content = []
+        process_nav_children(child["children"], temp_content, heading_level + 1)
+
+        # Only add the heading if there's actual content
+        if temp_content.any? { |line| line.start_with?("- ") || line.start_with?("#") }
+          # Add spacing before heading if there's previous content
+          content << "" unless content.empty? || content.last == ""
+
+          heading_prefix = "#" * [heading_level, 6].min # Max heading level is H6
+          content << "#{heading_prefix} #{child['name']}"
+          content << ""
+
+          content.concat(temp_content)
+        end
+      end
+    end
+  end
+
+  def should_skip_item?(item)
+    item["path"]&.include?("apis/graphql/schemas/") # Skip GraphQL schema docs
+  end
+end
diff --git a/app/models/page.rb b/app/models/page.rb
index 9bdff985dc3..2dda19e56f0 100644
--- a/app/models/page.rb
+++ b/app/models/page.rb
@@ -1,21 +1,21 @@
 # frozen_string_literal: true
 
 class Page
-  HEADING_REGEX = /^[#]{2}\s(.+)$/
+  HEADING_REGEX = /^\#{2}\s(.+)$/
 
   class << self
     # Find all markdown pages in the pages directory (ignoring partials).
     def all
       Dir.glob("#{Rails.root}/pages/**/*.md")
-        .select { |path | !path.to_s.include?("/_") }
-        .map do |path|
-          Struct.new(:path, :updated_at).new(
-            path
-              .sub("#{Rails.root}/pages/", "/docs/")
-              .sub(/\.md$/, "")
-              .gsub("_", "-"),
-            File.mtime(path)
-          )
+         .select { |path| !path.to_s.include?('/_') }
+         .map do |path|
+        Struct.new(:path, :updated_at).new(
+          path
+            .sub("#{Rails.root}/pages/", '/docs/')
+            .sub(/\.md$/, '')
+            .gsub('_', '-'),
+          File.mtime(path)
+        )
       end
     end
   end
@@ -30,10 +30,12 @@ def initialize(view_helpers: nil, image_path: '')
     end
 
     def estimated_time(description)
-      %{

Estimated time: #{description}

} + %(

Estimated time: #{description}

) end - def image(name, args={}) + def image(name, args = {}) + align = args.delete(:align)&.to_s + # Support the same :size that the standard Rails helper supports if size = args.delete(:size) width, height = size.split('x').map(&:to_i) @@ -43,61 +45,70 @@ def image(name, args={}) if args.include?(:width) && args.include?(:height) args[:max_width] = args[:width] + args[:align] = align if align responsive_image_tag(image_path(name), args[:width], args[:height], args.except(:width, :height)) else + if align == 'center' + # Add centering styles for non-responsive images + args[:style] = merge_styles(args[:style], 'display:block; margin-inline: auto;') + end @view_helpers.image_tag(image_path(name), args) end end def image_path(name) - stripped_image_path = @image_path.sub(/\Adocs\//, "") - @view_helpers.vite_asset_path(File.join("images", stripped_image_path, name)) + stripped_image_path = @image_path.sub(%r{\Adocs/}, '') + @view_helpers.vite_asset_path(File.join('images', stripped_image_path, name)) end def paginated_resource_docs_url @url_helpers.docs_path + '/rest-api#pagination' end - def url_helpers - @url_helpers - end + attr_reader :url_helpers def get_binding binding end + def merge_styles(*styles) + styles.compact.join(' ').strip.presence + end + def render(partial) - PagesController.render(partial: partial, formats: [:md, :html]) + PagesController.render(partial: partial, formats: %i[md html]) end def render_markdown(partial: nil, text: nil) - if partial.blank? && text.blank? - raise ArgumentError, "partial or nil not specified" - end + raise ArgumentError, 'partial or nil not specified' if partial.blank? && text.blank? text = if partial - render(partial) - else - text - end + render(partial) + else + text + end Page::Renderer.render(text).html_safe end - def responsive_image_tag(image, width, height, image_tag_options={}, &block) + def responsive_image_tag(image, width, height, image_tag_options = {}, &block) + align_center = image_tag_options.delete(:align)&.to_s == 'center' max_width = image_tag_options.delete(:max_width) - img_class = image_tag_options.delete(:class).try(:split, " ") || [] - img_class << "responsive-image-container" + img_class = image_tag_options.delete(:class).try(:split, ' ') || [] + img_class << 'responsive-image-container' - container = content_tag :div, image_tag(image, image_tag_options), class: img_class + container_style = align_center && !max_width ? 'margin-inline: auto;' : nil + container = content_tag :div, image_tag(image, image_tag_options), class: img_class, style: container_style if max_width - content_tag :div, container, style: "max-width: #{max_width}px" + outer_style = "max-width: #{max_width}px" + outer_style << '; margin-inline: auto' if align_center + content_tag :div, container, style: outer_style else container end @@ -122,15 +133,19 @@ def exists? end def sections - extracted_data.fetch("sections") + extracted_data.fetch('sections') end def markdown_body erb_renderer = ERB.new(file.content, trim_mode: '-') template_binding = TemplateBinding.new(view_helpers: @view, - image_path: File.join("docs", basename)) + image_path: File.join('docs', basename)) - erb_renderer.result(template_binding.get_binding) + result = erb_renderer.result(template_binding.get_binding) + + # Remove HTML comments from the markdown output + # This ensures they don't appear in .md format responses or copy functionality + filter_html_comments(result) end def body @@ -150,17 +165,17 @@ def is_canonical? end def basename - @name.to_s.gsub(/[^0-9a-zA-Z\-\_\/]/, '').underscore + @name.to_s.gsub(%r{[^0-9a-zA-Z\-_/]}, '').underscore end # Page title, either from front matter or extracted from the markdown def title - front_matter.fetch(:title, agentize_title(extracted_data.fetch("name"))) + front_matter.fetch(:title, agentize_title(extracted_data.fetch('name'))) end # Page description, either from front matter or extracted from the markdown def description - front_matter.fetch(:description, extracted_data.fetch("shortDescription")) + front_matter.fetch(:description, extracted_data.fetch('shortDescription')) end # Should page render a table of contents? @@ -174,7 +189,7 @@ def toc_include_h3? end def template - front_matter.fetch(:template, "show") + front_matter.fetch(:template, 'show') end # Returns focus keywords to guide content writers with an overview of the page content @@ -192,7 +207,7 @@ def front_matter # Default to rendering table of contents "toc": true, # Default to H3s being included in the table of contents - "toc_include_h3": true, + "toc_include_h3": true } if file.front_matter defaults.merge(file.front_matter.symbolize_keys) @@ -204,28 +219,39 @@ def front_matter def file @_file ||= ::FrontMatterParser::Parser.parse_file(filename) - rescue + rescue StandardError raise "Error parsing #{filename}" end def filename @filename ||= begin - directory = Rails.root.join("pages") + directory = Rails.root.join('pages') - potential_files = [ "#{basename}.md" ].map { |n| directory.join(n) } - potential_files.find { |file| File.exist?(file) } - end + potential_files = ["#{basename}.md"].map { |n| directory.join(n) } + potential_files.find { |file| File.exist?(file) } + end end def agentize_title(title) - if basename =~ /^agent\/v(.+?)\/?/ and basename.exclude?('elastic_ci') - "#{title} v#{$1}" + if basename =~ %r{^agent/v(.+?)/?} and basename.exclude?('elastic_ci') + "#{title} v#{::Regexp.last_match(1)}" else title end end def keywords_from_path - @view.request.path.split("/").reject(&:empty?).map { |segment| segment.gsub("-", " ") }.join(", ") + @view.request.path.split('/').reject(&:empty?).map { |segment| segment.gsub('-', ' ') }.join(', ') + end + + def filter_html_comments(text) + # Remove HTML comments using a regex that handles: + # - Single line comments: + # - Multi-line comments that span multiple lines + # - Comments with various content including newlines and special characters + result = text.gsub(//m, '') + + # Clean up any resulting blank lines (but preserve intentional spacing) + result.gsub(/\n\s*\n\s*\n/, "\n\n") end end diff --git a/app/models/page/buildkite_url.rb b/app/models/page/buildkite_url.rb index 541ce814913..a460cbd3513 100644 --- a/app/models/page/buildkite_url.rb +++ b/app/models/page/buildkite_url.rb @@ -4,7 +4,7 @@ def user_authorizations_url end def signup_path - "https://buildkite.com/signup" + "https://buildkite.com/signup?iaid=pricing-free" end def user_access_tokens_url diff --git a/app/models/page/renderer.rb b/app/models/page/renderer.rb index 857009dccb9..50c922ee720 100644 --- a/app/models/page/renderer.rb +++ b/app/models/page/renderer.rb @@ -17,6 +17,7 @@ def render(text, options = {}) doc = add_automatic_ids_to_headings(doc) doc = add_heading_anchor_links(doc) doc = fix_curl_highlighting(doc) + doc = remove_syntax_error_highlighting(doc) doc = add_code_filenames(doc) doc = add_callout(doc) doc = decorate_external_links(doc) @@ -123,6 +124,23 @@ def fix_curl_highlighting(doc) doc end + def remove_syntax_error_highlighting(doc) + # Remove all error highlighting spans from code blocks + # Rouge's syntax highlighters often incorrectly mark valid syntax as errors + doc.search('.//pre[contains(@class, "highlight")]').each do |pre| + code_block = pre.at('code') + next unless code_block + + # Replace error spans with plain text (unwrap the content) + code_block.inner_html = code_block.inner_html.gsub( + /(.+?)<\/span>/, + '\1' + ) + end + + doc + end + def add_code_filenames(doc) doc.search('./p').each do |node| next unless node.text.starts_with?('{: codeblock-file=') diff --git a/app/views/application/_analytics.html.erb b/app/views/application/_analytics.html.erb index 312e87bf2d3..f0ebbed1e8b 100644 --- a/app/views/application/_analytics.html.erb +++ b/app/views/application/_analytics.html.erb @@ -102,4 +102,12 @@ var getFirstSource = function() { }}(); <% end %> - <%= javascript_include_tag "https://buildkite.com/_site/scripts/utm.js", async: true %> +<%= javascript_tag nonce: true do %> + !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId get_distinct_id".split(" "),n=0;n \ No newline at end of file diff --git a/app/views/application/_brand.html.erb b/app/views/application/_brand.html.erb index 45211112894..498c4a72903 100644 --- a/app/views/application/_brand.html.erb +++ b/app/views/application/_brand.html.erb @@ -3,6 +3,9 @@ Buildkite logo <% end %> <%= link_to home_page_path, title: "Return to Docs index page" do %> - Buildkite Docs" /> + + + + <% end %> diff --git a/app/views/application/_footer.html.erb b/app/views/application/_footer.html.erb index 04a3d8e4706..99f14b3809b 100644 --- a/app/views/application/_footer.html.erb +++ b/app/views/application/_footer.html.erb @@ -14,7 +14,7 @@ Icon: pull request - Contribute an update + Edit this page
diff --git a/app/views/application/_global_links.html.erb b/app/views/application/_global_links.html.erb index 35b7f2546ce..01825308532 100644 --- a/app/views/application/_global_links.html.erb +++ b/app/views/application/_global_links.html.erb @@ -12,6 +12,22 @@ +
  • + +
  • <% if probably_authenticated? %> <% end %> diff --git a/app/views/application/_head.html.erb b/app/views/application/_head.html.erb index c815c443965..9daa07de4b3 100644 --- a/app/views/application/_head.html.erb +++ b/app/views/application/_head.html.erb @@ -1,10 +1,8 @@ <%= tag :link, rel: "dns-prefetch", href: ENV["ASSET_HOST"] if ENV["ASSET_HOST"] %> - - - - + + <%= [content_for(:page_title), "Buildkite Documentation"].compact.join(" | ") %> <% if description = content_for(:page_description) %> @@ -22,9 +20,13 @@ <%# Links to pages on the same origin, but outside /docs site shouldn't use Turbo %> -<%= tag :link, rel: "shortcut icon", href: vite_asset_path("images/favicon.png"), type: "image/x-icon" %> -<%= tag :link, rel: "apple-touch-icon", href: vite_asset_path("images/appicon.png") %> -<%= tag :link, rel: "mask-icon", href: vite_asset_path("images/logo-pinned.svg"), color: "#14CC80" %> + +<%= tag :link, rel: "icon", href: vite_asset_path("images/favicon.svg"), type: "image/svg+xml" %> +<%= tag :link, rel: "icon", href: vite_asset_path("images/favicon.png"), type: "image/png" %> +<%= tag :link, rel: "apple-touch-icon", href: "/apple-touch-icon.png" %> +<%= tag :meta, property: "theme-color", media:"(prefers-color-scheme: light)", content:"#FFFFFF" %> +<%= tag :meta, property: "theme-color", media:"(prefers-color-scheme: dark)", content:"#020617" %> +<%= tag :link, rel: "manifest", href: "/manifest.json", crossOrigin: "use-credentials" %> <%= tag :meta, property: "twitter:card", content: content_for(:page_twitter_card) || "summary_large_image" %> @@ -34,7 +36,7 @@ <%= tag :meta, property: "og:type", content: content_for(:page_og_type) || "website" %> <%= tag :meta, property: "og:title", content: content_for(:page_og_title) || content_for(:page_title) || "Buildkite"%> <%= tag :meta, property: "og:description", content: content_for(:page_og_description) || content_for(:page_description) || "Automate your team’s software development processes, from testing through to delivery, no matter the language, environment or toolchain." %> -<%= tag :meta, property: "og:image", content: content_for(:page_image) || image_path("opengraph_default.png").gsub(/^\/\//, 'https://') %> +<%= tag :meta, property: "og:image", content: content_for(:page_image) || "https://buildkite.com#{image_path("opengraph_default.png")}" %> <% if page_image_alt = content_for(:page_image_alt) %> <%= tag :meta, property: "og:image:alt", content: page_image_alt %> <% end %> @@ -53,6 +55,16 @@ <%= render 'helpscout' %> + + <% if ENV.fetch("DD_RUM_ENABLED", false) %>