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 [](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 (