diff --git a/.github/workflows/local-dev.yml b/.github/workflows/local-dev.yml
index e54d1375d95d..6653b5f41ba0 100644
--- a/.github/workflows/local-dev.yml
+++ b/.github/workflows/local-dev.yml
@@ -1,9 +1,7 @@
name: Local development
-# **What it does**: Can you start the local server like a writer would do?
-# **Why we have it**: Our CI is often heavily geared on testing in "production"
-# that historically we've been known to break local
-# development sometimes.
+# **What it does**: Basic smoke test to ensure local dev server starts and serves content
+# **Why we have it**: Catch catastrophic "npm start is completely broken" scenarios
# **Who does it impact**: Engineers, Contributors.
on:
@@ -28,76 +26,25 @@ jobs:
with:
token: ${{ secrets.DOCS_BOT_PAT_BASE }}
- # Note that we don't check out docs-early-access, Elasticsearch,
- # or any remote translations. Nothing fancy here!
-
- name: Disable Next.js telemetry
run: npx next telemetry disable
- - name: Install headless browser
- run: npx playwright install --no-shell
-
- # The Playwright test, with the env vars we set here, takes care of
- # starting a server and shutting it down when it's done.
- # That's why it's important this step comes before the `npm start &`
- # step below.
- - name: Run Playwright tests
- env:
- # This is what local dev contributors are expected to do.
- PLAYWRIGHT_START_SERVER_COMMAND: 'npm start'
- # This is so that timeouts aren't retried, which can lead to
- # tests not exiting at the end with a non-zero. Otherwise,
- # by default failures are marked as "flaky" instead of "failed".
- PLAYWRIGHT_RETRIES: 0
- TEST_EARLY_ACCESS: ${{ github.repository == 'github/docs-internal' }}
- # workaround for https://github.com/nodejs/node/issues/59364 as of 22.18.0
- NODE_OPTIONS: '--no-experimental-strip-types --max-old-space-size=8192'
- run: npm run playwright-test -- playwright-local-dev
-
- - name: Start server in the background
- run: npm start > /tmp/stdout.log 2> /tmp/stderr.log &
-
- - name: View the home page
- run: |
- echo "Going to sleep a little to wait for the server to start"
- sleep 15
- curl --fail --retry-connrefused --retry 5 http://localhost:4000/
-
- - name: Run basic tests
- run: npm run test-local-dev
-
- - if: ${{ failure() }}
- name: Debug server outputs on errors
+ - name: Start server and basic smoke test
run: |
- echo "____STDOUT____"
- cat /tmp/stdout.log
- echo "____STDERR____"
- cat /tmp/stderr.log
-
- - name: Pre-commit hooks should prevent bad Markdown edits
- run: |
- set -e
-
- # This test assumes this one file always exists
- ls content/get-started/start-your-journey/hello-world.md
-
- # Not sure if it matters but we're in a detached HEAD state
- # after the actions/checkout action.
- git checkout -b my-new-branch
- # Also, do this so you don't get errors from git about this
- # not being set up before your first commit attempt
- git config user.name github-actions
- git config user.email github-actions@github.com
-
- # To know what will fail the markdown lint, see src/content-linter/style/github-docs.js
- # Add some NOT valid Markdown to it
- # In this case an internal link with a hardcode /en/ prefix.
- echo "This *is** not valid [Markdown](/en/foo)" >> content/get-started/start-your-journey/hello-world.md
- git commit -a -m "this should fail"
- exit_code=$?
- if [ $exit_code != 0 ]; then
- echo "That SHOULD have failed, but it DIDN'T"
- exit 1
+ # Start server in background
+ npm start > /tmp/stdout.log 2> /tmp/stderr.log &
+ SERVER_PID=$!
+
+ # Wait for server to be ready and test homepage
+ if curl --fail --retry-connrefused --retry 10 --retry-delay 2 http://localhost:4000/; then
+ echo "✅ Local dev server started successfully and serves homepage"
+ kill $SERVER_PID 2>/dev/null || true
else
- echo "As expected, it failed :)"
+ echo "❌ Local dev server failed to start or serve content"
+ echo "____STDOUT____"
+ cat /tmp/stdout.log
+ echo "____STDERR____"
+ cat /tmp/stderr.log
+ kill $SERVER_PID 2>/dev/null || true
+ exit 1
fi
diff --git a/content/copilot/concepts/agents/coding-agent/about-coding-agent.md b/content/copilot/concepts/agents/coding-agent/about-coding-agent.md
index f0c2352c1957..448116e654fd 100644
--- a/content/copilot/concepts/agents/coding-agent/about-coding-agent.md
+++ b/content/copilot/concepts/agents/coding-agent/about-coding-agent.md
@@ -64,7 +64,15 @@ Having {% data variables.product.prodname_copilot_short %} as an additional codi
### Making {% data variables.copilot.copilot_coding_agent %} available
-Before you can assign tasks to {% data variables.product.prodname_copilot_short %}, it must be enabled. See [AUTOTITLE](/copilot/concepts/coding-agent/about-enabling-coding-agent).
+Before you can assign tasks to {% data variables.copilot.copilot_coding_agent %}, it must be enabled.
+
+{% data variables.copilot.copilot_coding_agent %} is available with the {% data variables.copilot.copilot_pro %}, {% data variables.copilot.copilot_pro_plus %}, {% data variables.copilot.copilot_for_business %} and {% data variables.copilot.copilot_enterprise %} plans.
+
+If you are a {% data variables.copilot.copilot_for_business %} or {% data variables.copilot.copilot_enterprise %} subscriber, an administrator must enable the relevant policy before you can use the agent.
+
+Repository owners can choose to opt out some or all repositories from {% data variables.copilot.copilot_coding_agent %}.
+
+For more information, see [AUTOTITLE](/copilot/concepts/agents/coding-agent/managing-access).
## {% data variables.copilot.copilot_coding_agent %} usage costs
diff --git a/content/copilot/concepts/agents/coding-agent/index.md b/content/copilot/concepts/agents/coding-agent/index.md
index 5ac72b1e63a0..e89377762dc1 100644
--- a/content/copilot/concepts/agents/coding-agent/index.md
+++ b/content/copilot/concepts/agents/coding-agent/index.md
@@ -9,7 +9,7 @@ topics:
- Copilot
children:
- /about-coding-agent
- - /coding-agent-for-business-and-enterprise
+ - /managing-access
- /mcp-and-coding-agent
contentType: concepts
redirect_from:
diff --git a/content/copilot/concepts/agents/coding-agent/coding-agent-for-business-and-enterprise.md b/content/copilot/concepts/agents/coding-agent/managing-access.md
similarity index 96%
rename from content/copilot/concepts/agents/coding-agent/coding-agent-for-business-and-enterprise.md
rename to content/copilot/concepts/agents/coding-agent/managing-access.md
index e183c6825014..678a48bea5c8 100644
--- a/content/copilot/concepts/agents/coding-agent/coding-agent-for-business-and-enterprise.md
+++ b/content/copilot/concepts/agents/coding-agent/managing-access.md
@@ -1,6 +1,6 @@
---
-title: About GitHub Copilot coding agent for business and enterprise
-shortTitle: Business and enterprise
+title: Managing access to GitHub Copilot coding agent
+shortTitle: Managing access
allowTitleToDifferFromFilename: true
intro: Find out about {% data variables.copilot.copilot_coding_agent %} policies available for {% data variables.copilot.copilot_enterprise %} and {% data variables.copilot.copilot_for_business %}, and about disabling the agent for specific repositories.
product: '{% data reusables.gated-features.copilot-coding-agent %}
Sign up for {% data variables.product.prodname_copilot_short %} {% octicon "link-external" height:16 %}'
@@ -17,6 +17,7 @@ redirect_from:
- /copilot/concepts/coding-agent/enable-coding-agent
- /copilot/concepts/agents/coding-agent/enable-coding-agent
- /copilot/how-tos/agents/copilot-coding-agent/enabling-copilot-coding-agent
+ - /copilot/concepts/agents/coding-agent/coding-agent-for-business-and-enterprise
contentType: concepts
---
diff --git a/content/copilot/concepts/context/spaces.md b/content/copilot/concepts/context/spaces.md
index c2622f2d12a4..7edd57ce15db 100644
--- a/content/copilot/concepts/context/spaces.md
+++ b/content/copilot/concepts/context/spaces.md
@@ -1,7 +1,7 @@
---
-title: About organizing and sharing context with GitHub Copilot Spaces
+title: About GitHub Copilot Spaces
shortTitle: Spaces
-intro: Understand how gathering context with {% data variables.copilot.copilot_spaces %} can improve your results and help your teammates.
+intro: Understand how organizing and sharing context with {% data variables.copilot.copilot_spaces %} can improve your {% data variables.copilot.copilot_chat_dotcom_short %} results and help your teammates.
permissions: Anyone with a {% data variables.product.prodname_copilot_short %} license can use {% data variables.copilot.copilot_spaces_short %}.
versions:
feature: copilot
diff --git a/package.json b/package.json
index b53e8f248c69..b08751c59f0d 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,6 @@
"sync-secret-scanning": "tsx src/secret-scanning/scripts/sync.ts",
"sync-webhooks": "npx tsx src/rest/scripts/update-files.ts -o webhooks",
"test": "vitest",
- "test-local-dev": "tsx src/workflows/test-local-dev.ts",
"test-moved-content": "tsx src/content-render/scripts/test-moved-content.ts",
"tsc": "tsc --noEmit",
"unallowed-contributions": "tsx src/workflows/unallowed-contributions.ts",
diff --git a/src/fixtures/tests/playwright-local-dev.spec.ts b/src/fixtures/tests/playwright-local-dev.spec.ts
deleted file mode 100644
index 8f85e6d8d69f..000000000000
--- a/src/fixtures/tests/playwright-local-dev.spec.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * These tests assume you have started the local dev server as a contributor
- * would. It does *not* use fixture data. It uses real English content
- * as seen in `main` or in the current branch. Therefore be careful
- * with what you can expect to find. Stick to known and stable content.
- *
- * It's always a risk that the content changes and can break tests
- * that exist to test the *code*. But these tests are ultimately there to
- * do what a human would do which is: Start the server, then open the
- * browser, then click around, then search, etc.
- *
- */
-
-import { test, expect } from '@playwright/test'
-import { dismissCTAPopover, turnOffExperimentsInPage } from '../helpers/turn-off-experiments'
-
-const TEST_EARLY_ACCESS = Boolean(JSON.parse(process.env.TEST_EARLY_ACCESS || 'false'))
-
-test('view home page', async ({ page }) => {
- await page.goto('/')
- await turnOffExperimentsInPage(page)
- await dismissCTAPopover(page)
-
- await expect(page).toHaveTitle(/GitHub Docs/)
-})
-
-test('click "Get started" from home page', async ({ page }) => {
- await page.goto('/')
- await turnOffExperimentsInPage(page)
- await dismissCTAPopover(page)
-
- await page.getByRole('link', { name: 'Get started' }).click()
- await expect(page).toHaveTitle(/Get started with GitHub/)
- await expect(page).toHaveURL(/\/en\/get-started/)
-})
-
-test('search "foo" and get results', async ({ page }) => {
- await page.goto('/')
- await turnOffExperimentsInPage(page)
- await dismissCTAPopover(page)
-
- await page.locator('[data-testid="search"]:visible').click()
- await page.getByTestId('overlay-search-input').fill('foo')
- // Wait for search results to load
- await page.waitForTimeout(1000)
- // Click "View more results" to get to the search page
- await page.getByText('View more results').click()
- await expect(page.getByRole('heading', { name: /\d+ Search results for "foo"/ })).toBeVisible()
-})
-
-test('view the early-access links page', async ({ page }) => {
- if (!TEST_EARLY_ACCESS) return
-
- await page.goto('/early-access')
- await turnOffExperimentsInPage(page)
- await dismissCTAPopover(page)
-
- await expect(page).toHaveURL(/\/en\/early-access/)
- await page.getByRole('heading', { name: 'Early Access documentation', level: 1 }).click()
- const links = await page.$$eval(
- '#article-contents ul li a',
- (elements: HTMLAnchorElement[]) => elements,
- )
- expect(links.length).toBeGreaterThan(0)
-})
diff --git a/src/fixtures/tests/playwright-rendering.spec.ts b/src/fixtures/tests/playwright-rendering.spec.ts
index e7d698761ecc..42f24f2012f8 100644
--- a/src/fixtures/tests/playwright-rendering.spec.ts
+++ b/src/fixtures/tests/playwright-rendering.spec.ts
@@ -199,6 +199,14 @@ test('search from enterprise-cloud and filter by top-level Fooing', async ({ pag
// for improvement!
})
+test('404 page renders correctly', async ({ page }) => {
+ const response = await page.goto('/this-definitely-does-not-exist')
+ expect(response?.status()).toBe(404)
+
+ // Check that the 404 page content is rendered
+ await expect(page.getByText(/It looks like this page doesn't exist/)).toBeVisible()
+})
+
test.describe('platform picker', () => {
test('switch operating systems', async ({ page }) => {
await page.goto('/get-started/liquid/platform-specific')
diff --git a/src/workflows/test-local-dev.ts b/src/workflows/test-local-dev.ts
deleted file mode 100755
index aff7dad53873..000000000000
--- a/src/workflows/test-local-dev.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import assert from 'node:assert/strict'
-import fs from 'fs'
-
-import cheerio from 'cheerio'
-
-/**
- * A very basic script that tests the local dev server.
- *
- * We use this in CI to make sure the local development server works.
- * There are certain things that only work and happen when in
- * local dev, that don't make sense to test in regular end-to-end tests
- * such as `vitest` rendering.
- *
- * For engineers to test this locally do the following:
- *
- * 1. Start `npm run dev` in one terminal
- * 2. Run `src/workflows/test-local-dev.ts` in another terminal
- *
- */
-
-main()
-
-async function get(path: string, options?: Record) {
- // By default, fetch() will follow redirects.
- const t0 = new Date()
- const response = await fetch(makeURL(path), options)
- const took = new Date().getTime() - t0.getTime()
- console.log(`GET ${path} => ${response.status} (${took}ms)`)
-
- // Convert fetch response to have similar interface as got response
- const body = await response.text()
- return {
- statusCode: response.status,
- body: body,
- }
-}
-
-function makeURL(path: string) {
- return `http://localhost:4000${path}`
-}
-
-async function main() {
- // Edit a page's content and expect to see that change when viewed
- await testEditingPage()
-
- // Only in local dev is the `?json=...` query string working
- await testJSONParameters()
-
- // In local development, it depends on proxying the search to prod
- // because if you haven't set up ELASTICSEARCH_URL.
- await testSiteSearch()
-
- await testViewingPages()
-
- // Next.js uses just-in-time compilation to compile pages on demand.
- // But once the server is up it should *not crash* to request these things.
- await testNextJsSpecialURLs()
-}
-
-async function testEditingPage() {
- const string = `Today's date is ${new Date().toString()}`
- const filePath = 'content/get-started/start-your-journey/hello-world.md'
- const content = fs.readFileSync(filePath, 'utf-8')
- try {
- fs.appendFileSync(filePath, string, 'utf-8')
-
- const res = await get('/get-started/start-your-journey/hello-world')
- if (!res.body.includes(string)) {
- throw new Error(`Couldn't find the string '${string}' in the response body`)
- }
- } finally {
- fs.writeFileSync(filePath, content, 'utf-8')
- }
-}
-
-async function testJSONParameters() {
- // currentVersion should be free-pro-team@latest
- {
- const res = await get('/get-started/start-your-journey/hello-world?json=currentVersion')
- const info = JSON.parse(res.body)
- assert(info === 'free-pro-team@latest')
- }
-
- // currentVersion should be free-pro-team@latest
- {
- const res = await get('/enterprise-server@latest/admin?json=currentVersion')
- const info = JSON.parse(res.body)
- assert(/enterprise-server@\d/.test(info))
- }
-
- // currentProduct (home page)
- {
- const res = await get('/en?json=currentProduct')
- const currentProduct = JSON.parse(res.body)
- assert(currentProduct === 'homepage')
- }
-
- // currentProduct (actions)
- {
- const res = await get('/en/actions?json=currentProduct')
- const currentProduct = JSON.parse(res.body)
- assert(currentProduct === 'actions')
- }
-
- // page
- {
- const res = await get('/en?json=page')
- const info = JSON.parse(res.body)
- assert(info.title === 'GitHub.com Help Documentation')
- }
-
- // Just ?json
- {
- const res = await get('/en?json')
- const info = JSON.parse(res.body)
- assert(info.message)
- assert(info.keys && Array.isArray(info.keys))
- }
-
- // Featured links
- {
- const res = await get('/en?json=featuredLinks.gettingStarted')
- const links = JSON.parse(res.body)
- assert(links && Array.isArray(links))
- assert(links[0].href)
- assert(links[0].page)
- }
-}
-
-async function testSiteSearch() {
- // Find something on free-pro-team@latest
- {
- const res = await get('/en/search?query=github')
- const $ = cheerio.load(res.body)
- // The [\d,]+ is because we use thousands separators in the number
- assert(/[\d,]+ Search results for "github"/.test($('h1').text()))
- assert($('[data-testid="search-result"]').length > 0)
- }
- // Find 0 things on enterprise-cloud@latest
- {
- const res = await get('/en/enterprise-cloud@latest/search?query=gobligook')
- const $ = cheerio.load(res.body)
- assert(/0 Search results for "gobligook"/.test($('h1').text()))
- assert($('[data-testid="search-result"]').length === 0)
- }
- // Using the search API
- {
- const res = await get('/api/search?query=github')
- const results = JSON.parse(res.body)
- assert(results.meta)
- assert(results.hits)
- }
-}
-
-async function testViewingPages() {
- // Getting a 404 page with an /en/ prefix should be a 404 HTML page
- const res = await get('/en/never/heard/of', {
- throwHttpErrors: false,
- })
- assert(res.statusCode === 404)
- // console.log(res.body)
- const $ = cheerio.load(res.body)
- assert(/It looks like this page doesn't exist./.test($('article').text()))
-}
-
-async function testNextJsSpecialURLs() {
- // _next/webpack-hmr
- {
- const res = await get('/_next/webpack-hmr')
- assert(res.statusCode === 200)
- }
- // _next/static/webpack/HASH.webpack.hot-update.json
- {
- const res = await get('/_next/static/webpack/deadbeefdeadbeef.webpack.hot-update.json')
- assert(res.statusCode === 200)
- }
-}