1
0
mirror of synced 2025-12-22 11:26:57 -05:00

Merge branch 'main' into code-nav-uses-tree-sitter

This commit is contained in:
Shati Patel
2021-01-25 20:54:25 +00:00
committed by GitHub
17 changed files with 271 additions and 77 deletions

View File

@@ -64,8 +64,11 @@ These labels operate cumulatively, so a self-hosted runners labels must match
### Routing precedence for self-hosted runners
If you use both repository-level and organization-level runners, {% data variables.product.prodname_dotcom %} follows an order of precedence when routing jobs to self-hosted runners:
When routing a job to a self-hosted runner, {% data variables.product.prodname_dotcom %} looks for a runner that matches the job's `runs-on` labels:
1. The job's `runs-on` labels are processed. {% data variables.product.prodname_dotcom %} then attempts to locate a runner that matches the label requirements:
2. The job is sent to a repository-level runner that matches the job labels. If no repository-level runner is available (either busy, offline, or no matching labels):
3. The job is sent to an organization-level runner that matches the job labels. If no organization-level runner is available, the job request fails with an error.
1. {% data variables.product.prodname_dotcom %} first searches for a runner at the repository level, then at the organization level{% if currentVersion ver_gt "enterprise-server@2.21" %}, then at the enterprise level{% endif %}.
2. The job is then sent to the first matching runner that is online and idle.
- If all matching online runners are busy, the job will queue at the level with the highest number of matching online runners.
- If all matching runners are offline, the job will queue at the level with the highest number of matching offline runners.
- If there are no matching runners at any level, the job will fail.
- If the job remains queued for more than 24 hours, the job will fail.

View File

@@ -215,6 +215,14 @@ c/o Corporation Service Company
2710 Gateway Oaks Drive, Suite 150N
Sacramento, CA 95833-3505
```
Under state and federal law, GitHub can seek reimbursement for costs associated with compliance with a valid legal demand, such as a subpoena, court order or search warrant. We only charge to recover some costs, and these reimbursements cover only a portion of the costs we actually incur to comply with legal orders.
While we do not charge in emergency situations or in other exigent circumstances, we seek reimbursement for all other legal requests in accordance with the following schedule, unless otherwise required by law:
- Initial search of up to 25 identifiers: Free
- Production of subscriber information/data for up to 5 accounts: Free
- Production of subscriber information/data for more than 5 accounts: $20 per account
- Secondary searches: $10 per search
Please make your requests as specific and narrow as possible, including the following information:

View File

@@ -842,6 +842,9 @@ _Teams_
- [`GET /repos/:owner/:repo/code-scanning/alerts`](/rest/reference/code-scanning#list-code-scanning-alerts-for-a-repository) (:read)
- [`GET /repos/:owner/:repo/code-scanning/alerts/:alert_id`](/rest/reference/code-scanning#get-a-code-scanning-alert) (:read)
- [`PATCH /repos/:owner/:repo/code-scanning/alerts/:alert_id`](/rest/reference/code-scanning#update-a-code-scanning-alert) (:write)
- [`GET /repos/:owner/:repo/code-scanning/analyses`](/rest/reference/code-scanning#list-recent-code-scanning-analyses-for-a-repository) (:read)
- [`POST /repos/:owner/:repo/code-scanning/sarifs`](/rest/reference/code-scanning#upload-a-sarif-file) (:write)
{% endif %}
{% if currentVersion == "free-pro-team@latest" %}

View File

@@ -160,3 +160,6 @@ product_sublanding:
tutorial: Tutorial
how_to: How-to guide
reference: Reference
learning_track_nav:
prevGuide: Previous Guide
nextGuide: Next Guide

View File

@@ -65,7 +65,10 @@
</div>
</div>
<div class="d-block border-top border-gray-light mt-4 markdown-body">
<div class="d-block mt-4 markdown-body">
{% if currentLearningTrack and currentLearningTrack.trackName %}
{% include learning-track-nav %}
{% endif %}
{% include helpfulness %}
{% unless page.hidden %}{% include contribution %}{% endunless %}
</div>

View File

@@ -0,0 +1,18 @@
<div class="py-3 px-4 rounded bg-white border-gradient--purple-pink d-flex flex-justify-between learning-track-nav">
{% assign track = currentLearningTrack %}
<span class="d-flex flex-column">
{% if track.prevGuide %}
<span class="f6 text-gray">{% data ui.learning_track_nav.prevGuide %}</span>
<a href="{{track.prevGuide.href}}?learn={{track.trackName}}" class="text-bold text-gray">{{track.prevGuide.title}}</a>
{% endif %}
</span>
<span class="d-flex flex-column flex-items-end">
{% if track.nextGuide %}
<span class="f6 text-gray">{% data ui.learning_track_nav.nextGuide %}</span>
<a href="{{track.nextGuide.href}}?learn={{track.trackName}}" class="text-bold text-gray text-right">{{track.nextGuide.title}}</a>
{% endif %}
</span>
</div>

View File

@@ -14,16 +14,18 @@ let $searchResultsContainer
let $searchOverlay
let $searchInput
// This is our default placeholder, but it can be localized with a <meta> tag
let placeholder = 'Search topics, products...'
let version
let language
export default function search () {
// First, only initialize search if the elements are on the page
$searchInputContainer = document.getElementById('search-input-container')
$searchResultsContainer = document.getElementById('search-results-container')
if (!$searchInputContainer || !$searchResultsContainer) return
// This overlay exists so if you click off the search, it closes
$searchOverlay = document.querySelector('.search-overlay-desktop')
// There's an index for every version/language combination
@@ -36,15 +38,25 @@ export default function search () {
placeholder = $placeholderMeta.content
}
// Write the search form into its container
$searchInputContainer.append(tmplSearchInput())
$searchInput = $searchInputContainer.querySelector('input')
searchWithYourKeyboard('#search-input-container input', '.ais-Hits-item')
toggleSearchDisplay()
// Prevent 'enter' from refreshing the page
$searchInputContainer.querySelector('form')
.addEventListener('submit', evt => evt.preventDefault())
// Search when the user finished typing
$searchInput.addEventListener('keyup', debounce(onSearch))
// Adds ability to navigate search results with keyboard (up, down, enter, esc)
searchWithYourKeyboard('#search-input-container input', '.ais-Hits-item')
// If the user already has a query in the URL, parse it and search away
parseExistingSearch()
// If not on home page, decide if search panel should be open
toggleSearchDisplay() // must come after parseExistingSearch
}
// The home page and 404 pages have a standalone search
@@ -64,43 +76,37 @@ function toggleSearchDisplay () {
// If not on homepage...
if (hasStandaloneSearch()) return
const $input = $searchInput
// Open panel if input is clicked
$searchInput.addEventListener('focus', openSearch)
// Open modal if input is clicked
$input.addEventListener('focus', () => {
openSearch()
})
// Close modal if overlay is clicked
// Close panel if overlay is clicked
if ($searchOverlay) {
$searchOverlay.addEventListener('click', () => {
closeSearch()
})
$searchOverlay.addEventListener('click', closeSearch)
}
// Open modal if page loads with query in the params/input
if ($input.value) {
// Open panel if page loads with query in the params/input
if ($searchInput.value) {
openSearch()
}
}
// On most pages, opens the search panel
function openSearch () {
$searchInput.classList.add('js-open')
$searchResultsContainer.classList.add('js-open')
$searchOverlay.classList.add('js-open')
}
// Close panel if not on homepage
function closeSearch () {
// Close modal if not on homepage
if (!hasStandaloneSearch()) {
$searchInput.classList.remove('js-open')
$searchResultsContainer.classList.remove('js-open')
$searchOverlay.classList.remove('js-open')
}
const $hits = $searchResultsContainer.querySelector('.ais-Hits')
if ($hits) $hits.style.display = 'none'
$searchInput.value = ''
onSearch()
}
function deriveLanguageCodeFromPath () {
@@ -122,6 +128,7 @@ function deriveVersionFromPath () {
: versionObject.miscBaseName
}
// Wait for the event to stop triggering for X milliseconds before responding
function debounce (fn, delay = 300) {
let timer
return (...args) => {
@@ -130,34 +137,47 @@ function debounce (fn, delay = 300) {
}
}
async function onSearch (evt) {
const query = evt.target.value
// When the user finishes typing, update the results
async function onSearch () {
const query = $searchInput.value
const url = new URL(location.origin)
url.pathname = '/search'
url.search = new URLSearchParams({ query, version, language }).toString()
// Update the URL with the search parameters in the query string
const pushUrl = new URL(location)
pushUrl.search = query ? new URLSearchParams({ query }) : ''
history.pushState({}, '', pushUrl)
const response = await fetch(url, {
// If there's a query, call the endpoint
// Otherwise, there's no results by default
let results = []
if (query.trim()) {
const endpointUrl = new URL(location.origin)
endpointUrl.pathname = '/search'
endpointUrl.search = new URLSearchParams({ language, version, query })
const response = await fetch(endpointUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const results = response.ok ? await response.json() : []
results = response.ok ? await response.json() : []
}
// Either way, update the display
$searchResultsContainer.querySelectorAll('*').forEach(el => el.remove())
$searchResultsContainer.append(
tmplSearchResults(results)
)
toggleStandaloneSearch()
// Analytics tracking
if (query.trim()) {
sendEvent({
type: 'search',
search_query: query
// search_context
})
}
}
// If on homepage, toggle results container if query is present
@@ -189,6 +209,14 @@ function toggleStandaloneSearch () {
if (queryPresent && $results) $results.style.display = 'block'
}
// If the user shows up with a query in the URL, go ahead and search for it
function parseExistingSearch () {
const params = new URLSearchParams(location.search)
if (!params.has('query')) return
$searchInput.value = params.get('query')
onSearch()
}
/** * Template functions ***/
function tmplSearchInput () {

View File

@@ -29,7 +29,7 @@
<div class="circle bg-white text-blue border border-white d-inline-flex">{% octicon "star-fill" height="24" class="v-align-middle m-2"%}</div>
<h3 class="font-mktg h2-mktg my-4">{{ featuredTrack.title }}</h3>
<div class="lead-mktg text-white f5 my-4">{{ featuredTrack.description }}</div>
<a class="d-inline-block border border-white text-white px-4 py-2 f5 no-underline text-bold" role="button" href="{{ featuredTrack.guides[0].href }}">
<a class="d-inline-block border border-white text-white px-4 py-2 f5 no-underline text-bold" role="button" href="{{ featuredTrack.guides[0].href }}?learn={{ featuredTrack.trackName }}">
<span class="mr-2">{% octicon "arrow-right" height="20" %}</span>
{% data ui.product_sublanding.start_path %}
</a>
@@ -37,7 +37,7 @@
</li>
{% for guide in featuredTrack.guides %}
<li class="px-2 d-flex flex-shrink-0">
<a href="{{ guide.href }}" class="d-inline-block Box p-5 bg-white border-gray no-underline">
<a href="{{ guide.href }}?learn={{ featuredTrack.trackName }}" class="d-inline-block Box p-5 bg-white border-gray no-underline">
<div class="d-flex flex-justify-between flex-items-center">
<div class="circle bg-white text-blue border-gradient--purple-pink d-inline-flex">
<span class="m-2 f2 lh-condensed-ultra text-center text-bold text-gradient--blue-purple" style="width: 24px; height: 24px;">{{ forloop.index }}</span>
@@ -61,7 +61,7 @@
<!-- Learning tracks -->
<div class="d-flex flex-wrap flex-items-start my-5">
{% for track in page.learningTracks offset:1 %}
<div class="my-3 px-0 px-4 col-12 col-md-6">
<div class="my-3 px-0 px-4 col-12 col-md-6 learning-track">
<div class="Box js-show-more-container">
<div class="Box-header bg-gradient--purple-pink py-4 d-flex flex-auto flex-items-start flex-wrap">
<div class="d-flex flex-auto flex-items-start col-8 col-md-12 col-xl-8">
@@ -75,14 +75,14 @@
<p class="text-white">{{ track.description }}</p>
</div>
</div>
<a class="d-inline-block border border-white text-white px-3 py-2 f5 no-underline text-bold no-wrap" role="button" href="{{ track.guides[0].href }}">
<a class="d-inline-block border border-white text-white px-3 py-2 f5 no-underline text-bold no-wrap" role="button" href="{{ track.guides[0].href }}?learn={{ track.trackName }}">
{% data ui.product_sublanding.start %}
<span class="ml-2">{% octicon "arrow-right" height="20" %}</span>
</a>
</div>
<div>
{% for guide in track.guides %}
<a class="Box-row d-flex flex-items-center text-gray-dark no-underline js-show-more-item {% if forloop.index > 4 %}d-none{% endif %}" href="{{ guide.href }}">
<a class="Box-row d-flex flex-items-center text-gray-dark no-underline js-show-more-item {% if forloop.index > 4 %}d-none{% endif %}" href="{{ guide.href }}?learn={{ track.trackName }}">
<div class="circle bg-gray d-inline-flex mr-4">
<span class="m-2 f3 lh-condensed-ultra text-center text-bold text-gradient--purple-pink" style="min-width: 20px; height: 20px;">{{ forloop.index }}</span>
</div>

View File

@@ -5,28 +5,45 @@ const removeFPTFromPath = require('./remove-fpt-from-path')
// rawLinks is an array of paths: [ '/foo' ]
// we need to convert it to an array of localized objects: [ { href: '/en/foo', title: 'Foo', intro: 'Description here' } ]
module.exports = async (rawLinks, context) => {
module.exports = async (rawLinks, context, option = { title: true, intro: true }) => {
if (!rawLinks) return
if (typeof rawLinks === 'string') {
return await processLink(rawLinks, context, option)
}
const links = []
for (const link of rawLinks) {
const linkObj = await processLink(link, context, option)
if (!linkObj) {
continue
} else {
links.push(linkObj)
}
}
return links
}
const processLink = async (link, context, option) => {
const linkPath = link.href || link
const version = context.currentVersion === 'homepage' ? nonEnterpriseDefaultVersion : context.currentVersion
const href = removeFPTFromPath(path.join('/', context.currentLanguage, version, linkPath))
const linkedPage = findPage(href, context.pages, context.redirects)
if (!linkedPage) continue
if (!linkedPage) return null
const opts = { textOnly: true, encodeEntities: true }
links.push({
href,
title: await linkedPage.renderTitle(context, opts),
intro: await linkedPage.renderProp('intro', context, opts),
page: linkedPage
})
const result = { href, page: linkedPage }
if (option.title) {
result.title = await linkedPage.renderTitle(context, opts)
}
return links
if (option.intro) {
result.intro = await linkedPage.renderProp('intro', context, opts)
}
return result
}

View File

@@ -208,6 +208,7 @@ class Page {
const track = context.site.data['learning-tracks'][context.currentProduct][trackName]
if (!track) continue
learningTracks.push({
trackName,
title: await renderContent(track.title, context, { textOnly: true, encodeEntities: true }),
description: await renderContent(track.description, context, { textOnly: true, encodeEntities: true }),
guides: await getLinkData(track.guides, context)

View File

@@ -84,6 +84,7 @@ module.exports = function (app) {
app.use(require('./enterprise-server-releases'))
app.use(require('./dev-toc'))
app.use(require('./featured-links'))
app.use(require('./learning-track'))
// *** Rendering, must go last ***
app.get('/*', asyncMiddleware(require('./render-page')))

View File

@@ -0,0 +1,40 @@
const { getPathWithoutLanguage, getPathWithoutVersion } = require('../lib/path-utils')
const getLinkData = require('../lib/get-link-data')
module.exports = async (req, res, next) => {
const noTrack = () => {
req.context.currentLearningTrack = {}
return next()
}
if (!req.context.page) return next()
const trackName = req.query.learn
if (!trackName) return noTrack()
const track = req.context.site.data['learning-tracks'][req.context.currentProduct][trackName]
if (!track) return noTrack()
const currentLearningTrack = { trackName }
const guidePath = getPathWithoutLanguage(getPathWithoutVersion(req.path))
const guideIndex = track.guides.findIndex((path) => path === guidePath)
if (guideIndex < 0) return noTrack()
if (guideIndex > 0) {
const prevGuidePath = track.guides[guideIndex - 1]
const { href, title } = await getLinkData(prevGuidePath, req.context, { title: true, intro: false })
currentLearningTrack.prevGuide = { href, title }
}
if (guideIndex < track.guides.length - 1) {
const nextGuidePath = track.guides[guideIndex + 1]
const { href, title } = await getLinkData(nextGuidePath, req.context, { title: true, intro: false })
currentLearningTrack.nextGuide = { href, title }
}
req.context.currentLearningTrack = currentLearningTrack
return next()
}

View File

@@ -18,11 +18,21 @@ const pageCache = new RedisAccessor({
allowSetFailures: true
})
// a list of query params that *do* alter the rendered page, and therefore should be cached separately
const cacheableQueries = ['learn']
module.exports = async function renderPage (req, res, next) {
const page = req.context.page
// Remove any query string (?...) and/or fragment identifier (#...)
const originalUrl = new URL(req.originalUrl, 'https://docs.github.com').pathname
const { pathname, searchParams } = new URL(req.originalUrl, 'https://docs.github.com')
for (const queryKey in req.query) {
if (!cacheableQueries.includes(queryKey)) {
searchParams.delete(queryKey)
}
}
const originalUrl = pathname + ([...searchParams].length > 0 ? `?${searchParams}` : '')
// Serve from the cache if possible (skip during tests)
const isCacheable = !process.env.CI && process.env.NODE_ENV !== 'test' && req.method === 'GET'

View File

@@ -72,7 +72,6 @@
"parse5": "^6.0.1",
"platform-utils": "^1.2.0",
"port-used": "^2.0.8",
"querystring": "^0.2.0",
"rate-limit-redis": "^2.0.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",

View File

@@ -32,7 +32,6 @@ const main = async () => {
'@babel/*',
'babel-preset-env',
'@primer/*',
'querystring',
'pa11y-ci',
'sass',
'babel-loader',

View File

@@ -1,6 +1,5 @@
/* global page, browser */
const sleep = require('await-sleep')
const querystring = require('querystring')
const { latest } = require('../../lib/enterprise-server-releases')
describe('homepage', () => {
@@ -12,7 +11,7 @@ describe('homepage', () => {
})
})
describe('algolia browser search', () => {
describe('browser search', () => {
jest.setTimeout(60 * 1000)
it('works on the homepage', async () => {
@@ -42,18 +41,18 @@ describe('algolia browser search', () => {
expect(hits.length).toBeGreaterThan(5)
})
it('sends the correct data to algolia for Enterprise Server', async () => {
it('sends the correct data to search for Enterprise Server', async () => {
expect.assertions(2)
const newPage = await browser.newPage()
await newPage.goto('http://localhost:4001/ja/enterprise/2.22/admin/installation')
await newPage.goto('http://localhost:4001/ja/enterprise-server@2.22/admin/installation')
await newPage.setRequestInterception(true)
newPage.on('request', interceptedRequest => {
if (interceptedRequest.method() === 'GET' && /search/i.test(interceptedRequest.url())) {
const { version, language } = querystring.parse(interceptedRequest.url())
expect(version).toBe('2.22')
expect(language).toBe('ja')
const { searchParams } = new URL(interceptedRequest.url())
expect(searchParams.get('version')).toBe('2.22')
expect(searchParams.get('language')).toBe('ja')
}
interceptedRequest.continue()
})
@@ -63,7 +62,7 @@ describe('algolia browser search', () => {
await newPage.waitForSelector('.search-result')
})
it('sends the correct data to algolia for GHAE', async () => {
it('sends the correct data to search for GHAE', async () => {
expect.assertions(2)
const newPage = await browser.newPage()
@@ -72,9 +71,9 @@ describe('algolia browser search', () => {
await newPage.setRequestInterception(true)
newPage.on('request', interceptedRequest => {
if (interceptedRequest.method() === 'GET' && /search/i.test(interceptedRequest.url())) {
const { version, language } = querystring.parse(interceptedRequest.url())
expect(version).toBe('ghae')
expect(language).toBe('en')
const { searchParams } = new URL(interceptedRequest.url())
expect(searchParams.get('version')).toBe('ghae')
expect(searchParams.get('language')).toBe('en')
}
interceptedRequest.continue()
})

View File

@@ -0,0 +1,62 @@
const { getDOM } = require('../helpers/supertest')
jest.setTimeout(3 * 60 * 1000)
describe('learning tracks', () => {
test('render first track as feature track', async () => {
const $ = await getDOM('/en/actions/guides')
expect($('.feature-track')).toHaveLength(1)
const href = $('.feature-track li a').first().attr('href')
const found = href.match(/.*\?learn=(.*)/i)
expect(found).not.toBeNull()
const trackName = found[1]
// check all the links contain track name
$('.feature-track li a').each((i, elem) => {
expect($(elem).attr('href')).toEqual(expect.stringContaining(`?learn=${trackName}`))
})
})
test('render other tracks', async () => {
const $ = await getDOM('/en/actions/guides')
expect($('.learning-track').length).toBeGreaterThanOrEqual(4)
$('.learning-track').each((i, trackElem) => {
const href = $(trackElem).find('.Box-header a').first().attr('href')
const found = href.match(/.*\?learn=(.*)/i)
expect(found).not.toBeNull()
const trackName = found[1]
// check all the links contain track name
$(trackElem).find('a.Box-row').each((i, elem) => {
expect($(elem).attr('href')).toEqual(expect.stringContaining(`?learn=${trackName}`))
})
})
})
})
describe('navigation banner', () => {
test('render navigation banner when url includes correct learning track name', async () => {
const $ = await getDOM('/en/actions/guides/setting-up-continuous-integration-using-workflow-templates?learn=continuous_integration')
expect($('.learning-track-nav')).toHaveLength(1)
const $navLinks = $('.learning-track-nav a')
expect($navLinks).toHaveLength(2)
$navLinks.each((i, elem) => {
expect($(elem).attr('href')).toEqual(expect.stringContaining('?learn=continuous_integration'))
})
})
test('does not include banner when url does not include `learn` param', async () => {
const $ = await getDOM('/en/actions/guides/setting-up-continuous-integration-using-workflow-templates')
expect($('.learning-track-nav')).toHaveLength(0)
})
test('does not include banner when url has incorrect `learn` param', async () => {
const $ = await getDOM('/en/actions/guides/setting-up-continuous-integration-using-workflow-templates?learn=not_real')
expect($('.learning-track-nav')).toHaveLength(0)
})
test('does not include banner when url is not part of the learning track', async () => {
const $ = await getDOM('/en/actions/learn-github-actions/introduction-to-github-actions?learn=continuous_integration')
expect($('.learning-track-nav')).toHaveLength(0)
})
})