Merge branch 'main' into deployment-guides/add-gke
This commit is contained in:
30
.github/workflows/repo-sync.yml
vendored
30
.github/workflows/repo-sync.yml
vendored
@@ -44,6 +44,7 @@ jobs:
|
||||
github_token: ${{ secrets.OCTOMERGER_PAT_WITH_REPO_AND_WORKFLOW_SCOPE }}
|
||||
|
||||
- name: Create pull request
|
||||
id: create-pull
|
||||
uses: repo-sync/pull-request@33777245b1aace1a58c87a29c90321aa7a74bd7d
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.OCTOMERGER_PAT_WITH_REPO_AND_WORKFLOW_SCOPE }}
|
||||
@@ -70,6 +71,35 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
number: ${{ steps.find-pull-request.outputs.number }}
|
||||
|
||||
# There are cases where the branch becomes out-of-date in between the time this workflow began and when the pull request is created/updated
|
||||
- name: Update branch
|
||||
if: ${{ steps.find-pull-request.outputs.number }}
|
||||
uses: actions/github-script@626af12fe9a53dc2972b48385e7fe7dec79145c9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const mainHeadSha = await github.git.getRef({
|
||||
...context.repo,
|
||||
ref: 'heads/main'
|
||||
})
|
||||
console.log(`heads/main sha: ${mainHeadSha.data.object.sha}`)
|
||||
|
||||
const pull = await github.pulls.get({
|
||||
...context.repo,
|
||||
pull_number: parseInt(${{ steps.find-pull-request.outputs.number }})
|
||||
})
|
||||
console.log(`Pull request base sha: ${pull.data.base.sha}`)
|
||||
|
||||
if (mainHeadSha.data.object.sha !== pull.data.base.sha || pull.data.mergeable_state === 'behind') {
|
||||
const updateBranch = await github.pulls.updateBranch({
|
||||
...context.repo,
|
||||
pull_number: parseInt(${{ steps.find-pull-request.outputs.number }})
|
||||
})
|
||||
console.log(updateBranch.data.message)
|
||||
} else {
|
||||
console.log(`Branch is already up-to-date`)
|
||||
}
|
||||
|
||||
- name: Send Slack notification if workflow fails
|
||||
uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd
|
||||
if: failure()
|
||||
|
||||
@@ -152,7 +152,7 @@ We (usually the docs team, but sometimes GitHub product managers, engineers, or
|
||||
You should always review your own PR first.
|
||||
|
||||
For content changes, make sure that you:
|
||||
- [ ] Confirm that the changes address every part of the content strategy plan from your issue (if there are differences, explain them).
|
||||
- [ ] Confirm that the changes address every part of the content design plan from your issue (if there are differences, explain them).
|
||||
- [ ] Review the content for technical accuracy.
|
||||
- [ ] Review the entire pull request using the [localization checklist](contributing/localization-checklist.md).
|
||||
- [ ] Copy-edit the changes for grammar, spelling, and adherence to the style guide.
|
||||
|
||||
3
app.json
3
app.json
@@ -3,10 +3,9 @@
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_PRODUCTION": "true",
|
||||
"ENABLED_LANGUAGES": "en, de"
|
||||
"ENABLED_LANGUAGES": "en"
|
||||
},
|
||||
"buildpacks": [
|
||||
{ "url": "https://github.com/DataDog/heroku-buildpack-datadog.git#1.21" },
|
||||
{ "url": "heroku/nodejs" }
|
||||
],
|
||||
"formation": {
|
||||
|
||||
@@ -79,8 +79,8 @@ Permission | Description
|
||||
[`single_file`](/rest/reference/permissions-required-for-github-apps/#permission-on-single-file) | Grants access to the [Contents API](/rest/reference/repos#contents). Can be one of: `none`, `read`, or `write`.
|
||||
[`starring`](/rest/reference/permissions-required-for-github-apps/#permission-on-starring) | Grants access to the [Starring API](/rest/reference/activity#starring). Can be one of: `none`, `read`, or `write`.
|
||||
[`statuses`](/rest/reference/permissions-required-for-github-apps/#permission-on-statuses) | Grants access to the [Statuses API](/rest/reference/repos#statuses). Can be one of: `none`, `read`, or `write`.
|
||||
[`team_discussions`](/rest/reference/permissions-required-for-github-apps/#permission-on-team-discussions) | Grants access to the [Team Discussions API](/rest/reference/teams#discussions) and the [Team Discussion Comments API](/rest/reference/teams#discussion-comments). Can be one of: `none`, `read`, or `write`.
|
||||
`vulnerability_alerts`| Grants access to receive security alerts for vulnerable dependencies in a repository. See "[About security alerts for vulnerable dependencies](/articles/about-security-alerts-for-vulnerable-dependencies)" to learn more. Can be one of: `none` or `read`.
|
||||
[`team_discussions`](/rest/reference/permissions-required-for-github-apps/#permission-on-team-discussions) | Grants access to the [Team Discussions API](/rest/reference/teams#discussions) and the [Team Discussion Comments API](/rest/reference/teams#discussion-comments). Can be one of: `none`, `read`, or `write`.{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@1.19" %}
|
||||
`vulnerability_alerts`| Grants access to receive security alerts for vulnerable dependencies in a repository. See "[About security alerts for vulnerable dependencies](/articles/about-security-alerts-for-vulnerable-dependencies)" to learn more. Can be one of: `none` or `read`.{% endif %}
|
||||
`watching` | Grants access to list and change repositories a user is subscribed to. Can be one of: `none`, `read`, or `write`.
|
||||
|
||||
### {% data variables.product.prodname_github_app %} webhook events
|
||||
|
||||
@@ -67,7 +67,7 @@ If the user accepts your request, GitHub redirects back to your site with a temp
|
||||
|
||||
{% endnote %}
|
||||
|
||||
Exchange this `code` for an access token. {% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@2.21" or currentVersion == "github-ae@latest" %} When expiring tokens are enabled, the access token expires in 8 hours and the refresh token expires in 6 months. Every time you refresh the token, you get a new refresh token. For more information, see "[Refreshing user-to-server access tokens](/developers/apps/refreshing-user-to-server-access-tokens)."
|
||||
Exchange this `code` for an access token. {% if currentVersion == "free-pro-team@latest" %} When expiring tokens are enabled, the access token expires in 8 hours and the refresh token expires in 6 months. Every time you refresh the token, you get a new refresh token. For more information, see "[Refreshing user-to-server access tokens](/developers/apps/refreshing-user-to-server-access-tokens)."
|
||||
|
||||
Expiring user tokens are currently part of the user-to-server token expiration beta and subject to change. To opt-in to the user-to-server token expiration beta feature, see "[Activating beta features for apps](/developers/apps/activating-beta-features-for-apps)."{% endif %}
|
||||
|
||||
|
||||
@@ -7,13 +7,11 @@ versions:
|
||||
free-pro-team: '*'
|
||||
---
|
||||
|
||||
{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@2.21" %}
|
||||
{% note %}
|
||||
|
||||
**Note:** {% data reusables.pre-release-program.suspend-installation-beta %}
|
||||
|
||||
{% endnote %}
|
||||
{% endif %}
|
||||
|
||||
### Suspending a GitHub App
|
||||
|
||||
|
||||
@@ -4,6 +4,5 @@ intro: 'You can list free and paid tools for developers to use in {% data variab
|
||||
mapTopic: true
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
---
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Customers can start a free trial for any paid plan on a Marketplace listing that
|
||||
|
||||
Free trials have a fixed length of 14 days. Customers are notified 4 days before the end of their trial period (on day 11 of the free trial) that their plan will be upgraded. At the end of a free trial, customers will be auto-enrolled into the plan they are trialing if they do not cancel.
|
||||
|
||||
For more information, see: "[Handling new purchases and free trials](/developers/github-marketplace/integrating-with-the-github-marketplace-api/handling-new-purchases-and-free-trials/)."
|
||||
For more information, see: "[Handling new purchases and free trials](/developers/github-marketplace/handling-new-purchases-and-free-trials/)."
|
||||
|
||||
{% note %}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ In addition to the requirements for all apps above, each app that you offer as a
|
||||
- {% data variables.product.prodname_github_app %}s should have a minimum of 100 installations.
|
||||
- {% data variables.product.prodname_oauth_app %}s should have a minimum of 200 users.
|
||||
- All paid apps must handle {% data variables.product.prodname_marketplace %} purchase events for new purchases, upgrades, downgrades, cancellations, and free trials. For more information, see "[Billing requirements for paid apps](#billing-requirements-for-paid-apps)" below.
|
||||
- Publishing organizations must have a verified domain and must enable two-factor authentication. For more information, see "[Requiring two-factor authentication in your organization](/github/setting-up-and-managing-organizations-and-teams/requiring-two-factor-authentication-in-your-organization.")
|
||||
- Publishing organizations must have a verified domain and must enable two-factor authentication. For more information, see "[Requiring two-factor authentication in your organization](/github/setting-up-and-managing-organizations-and-teams/requiring-two-factor-authentication-in-your-organization)."
|
||||
|
||||
When you are ready to publish the app on {% data variables.product.prodname_marketplace %} you must request verification for the listing.
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ If you don't want to use SSH keys, you can use [HTTPS with OAuth tokens][git-aut
|
||||
* Users don't have to change their local SSH settings.
|
||||
* Multiple tokens (one for each user) are not needed; one token per server is enough.
|
||||
* A token can be revoked at any time, turning it essentially into a one-use password.
|
||||
{% if enterpriseServerVersions contains currentVersion %}
|
||||
* Generating new tokens can be easily scripted using [the OAuth API](/rest/reference/oauth-authorizations#create-a-new-authorization).
|
||||
{% endif %}
|
||||
|
||||
##### Cons
|
||||
|
||||
|
||||
@@ -430,7 +430,7 @@ Key | Type | Description
|
||||
|
||||
{% endnote %}
|
||||
|
||||
{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@2.21" or currentVersion == "github-ae@latest" %}
|
||||
{% if currentVersion == "free-pro-team@latest" %}
|
||||
{% note %}
|
||||
|
||||
**Note:** {% data reusables.pre-release-program.suspend-installation-beta %} For more information, see "[Suspending a {% data variables.product.prodname_github_app %} installation](/apps/managing-github-apps/suspending-a-github-app-installation/)."
|
||||
@@ -1124,9 +1124,11 @@ Key | Type | Description
|
||||
{{ webhookPayloadsForCurrentVersion.secret_scanning_alert.reopened }}
|
||||
{% endif %}
|
||||
|
||||
{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@1.19" %}
|
||||
### security_advisory
|
||||
|
||||
Activity related to a security advisory. A security advisory provides information about security-related vulnerabilities in software on GitHub. The security advisory dataset also powers the GitHub security alerts, see "[About security alerts for vulnerable dependencies](/articles/about-security-alerts-for-vulnerable-dependencies/)."
|
||||
{% endif %}
|
||||
|
||||
#### Availability
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ When an {% data variables.product.prodname_oauth_app %} wants to identify you by
|
||||
|
||||
*Scopes* are named groups of permissions that an {% data variables.product.prodname_oauth_app %} can request to access both public and non-public data.
|
||||
|
||||
When you want to use an {% data variables.product.prodname_oauth_app %} that integrates with {% data variables.product.product_name %}, that app lets you know what type of access to your data will be required. If you grant access to the app, then the app will be able to perform actions on your behalf, such as reading or modifying data. For example, if you want to use an app that requests `user:email` scope, the app will have read-only access to your private email addresses. For more information, see "[About scopes for {% data variables.product.prodname_oauth_app %}s](//apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps)."
|
||||
When you want to use an {% data variables.product.prodname_oauth_app %} that integrates with {% data variables.product.product_name %}, that app lets you know what type of access to your data will be required. If you grant access to the app, then the app will be able to perform actions on your behalf, such as reading or modifying data. For example, if you want to use an app that requests `user:email` scope, the app will have read-only access to your private email addresses. For more information, see "[About scopes for {% data variables.product.prodname_oauth_app %}s](/apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps)."
|
||||
|
||||
{% tip %}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Applications can have *read* or *write* access to your {% data variables.product
|
||||
|
||||
*Scopes* are named groups of permissions that an application can request to access both public and non-public data.
|
||||
|
||||
When you want to use a third-party application that integrates with {% data variables.product.product_name %}, that application lets you know what type of access to your data will be required. If you grant access to the application, then the application will be able to perform actions on your behalf, such as reading or modifying data. For example, if you want to use an app that requests `user:email` scope, the app will have read-only access to your private email addresses. For more information, see "[About scopes for {% data variables.product.prodname_oauth_app %}s](//apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps)."
|
||||
When you want to use a third-party application that integrates with {% data variables.product.product_name %}, that application lets you know what type of access to your data will be required. If you grant access to the application, then the application will be able to perform actions on your behalf, such as reading or modifying data. For example, if you want to use an app that requests `user:email` scope, the app will have read-only access to your private email addresses. For more information, see "[About scopes for {% data variables.product.prodname_oauth_app %}s](/apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps)."
|
||||
|
||||
{% tip %}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ On Windows, the `codeql-runner-win.exe` file usually requires no change to permi
|
||||
Once you have downloaded the {% data variables.product.prodname_codeql_runner %} and verified that it can be executed, you should make the runner available to each CI server that you intend to use for {% data variables.product.prodname_code_scanning %}. It is important to notice that each CI server that you intend to use for {% data variables.product.prodname_code_scanning %} needs to have the {% data variables.product.prodname_codeql_runner %}. You might configure each server to copy the runner from a central, internal location, or you could use the REST API to get the runner direct from GitHub, for example:
|
||||
|
||||
```shell
|
||||
wget https://github.com/github/codeql-action/releases/download/codeql-bundle-20200826/codeql-runner-linux
|
||||
wget https://github.com/github/codeql-action/releases/latest/download/codeql-runner-linux
|
||||
chmod +x codeql-runner-linux
|
||||
```
|
||||
|
||||
@@ -127,7 +127,7 @@ This example is similar to the previous example, however this time the repositor
|
||||
> ...
|
||||
> CodeQL environment output to "/srv/checkout/example-repo-2/codeql-runner/codeql-env.json"
|
||||
and "/srv/checkout/example-repo-2/codeql-runner/codeql-env.sh".
|
||||
Please export these variables to future processes so the build can be traced, for example by running "
|
||||
Please export these variables to future processes so that CodeQL can monitor the build, for example by running "
|
||||
. /srv/checkout/example-repo-2/codeql-runner/codeql-env.sh".
|
||||
```
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ To help you understand your subscriptions and decide whether to unsubscribe, see
|
||||
|
||||
{% note %}
|
||||
|
||||
**Note:** Instead of unsubscribing, you have the option to ignore a repository. If you ignore a repository, you won't receive any notifications. We don't recommend ignoring repositories as you won't be notified if you're @mentioned. {% if currentVersion == "free-pro-team@latest" %}If you're experiencing abuse and want to ignore a repository, please [contact support](/contact) so we can help. {% data reusables.policies.abuse %}{% endif %}
|
||||
**Note:** Instead of unsubscribing, you have the option to ignore a repository. If you ignore a repository, you won't receive any notifications. We don't recommend ignoring repositories as you won't be notified if you're @mentioned. {% if currentVersion == "free-pro-team@latest" %}If you're experiencing abuse and want to ignore a repository, please contact {% data variables.contact.contact_support %} so we can help. {% data reusables.policies.abuse %}{% endif %}
|
||||
|
||||
{% endnote %}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ You can also watch and unwatch releases in a repository. For more information, s
|
||||
|
||||
{% note %}
|
||||
|
||||
**Note:** You can also choose to ignore a repository. If you ignore a repository, you won't receive any notifications. We don't recommend ignoring repositories as you won't be notified if you're @mentioned. {% if currentVersion == "free-pro-team@latest" %}If you experiencing abuse and want to ignore a repository, please [contact support](/contact) so we can help. {% data reusables.policies.abuse %}{% endif %}
|
||||
**Note:** You can also choose to ignore a repository. If you ignore a repository, you won't receive any notifications. We don't recommend ignoring repositories as you won't be notified if you're @mentioned. {% if currentVersion == "free-pro-team@latest" %}If you experiencing abuse and want to ignore a repository, please contact {% data variables.contact.contact_support %} so we can help. {% data reusables.policies.abuse %}{% endif %}
|
||||
|
||||
{% endnote %}
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ Due to the complexity of searching code, there are some restrictions on how sear
|
||||
- Only the _default branch_ is indexed for code search.{% if currentVersion == "free-pro-team@latest" %}
|
||||
- Only files smaller than 384 KB are searchable.{% else %}* Only files smaller than 5 MB are searchable.
|
||||
- Only the first 500 KB of each file is searchable.{% endif %}
|
||||
- Only repositories with fewer than 500,000 files are searchable.
|
||||
- Only repositories with fewer than 500,000 files are searchable.{% if currentVersion == "free-pro-team@latest" %}
|
||||
- Only repositories that have had activity or have been returned in search results in the last year are searchable.{% endif %}
|
||||
- Except with [`filename`](#search-by-filename) searches, you must always include at least one search term when searching source code. For example, searching for [`language:javascript`](https://github.com/search?utf8=%E2%9C%93&q=language%3Ajavascript&type=Code&ref=searchresults) is not valid, while [`amazing language:javascript`](https://github.com/search?utf8=%E2%9C%93&q=amazing+language%3Ajavascript&type=Code&ref=searchresults) is.
|
||||
- At most, search results can show two fragments from the same file, but there may be more results within the file.
|
||||
- You can't use the following wildcard characters as part of your search query: <code>. , : ; / \ ` ' " = * ! ? # $ & + ^ | ~ < > ( ) { } [ ]</code>. The search will simply ignore these symbols.
|
||||
|
||||
@@ -4,8 +4,11 @@ intro: '{% data variables.product.prodname_insights %} analyzes your {% data var
|
||||
product: '{% data reusables.gated-features.github-insights %}'
|
||||
redirect_from:
|
||||
- /github/installing-and-configuring-github-insights/github-insights-and-data-protection-for-your-organization
|
||||
- /enterprise-server@2.22/github/site-policy/github-insights-and-data-protection-for-your-organization
|
||||
- /enterprise-server@2.21/github/site-policy/github-insights-and-data-protection-for-your-organization
|
||||
- /enterprise-server@2.20/github/site-policy/github-insights-and-data-protection-for-your-organization
|
||||
versions:
|
||||
enterprise-server: '*'
|
||||
free-pro-team: '*'
|
||||
---
|
||||
|
||||
For more information about the terms that govern {% data variables.product.prodname_insights %}, see your {% data variables.product.prodname_ghe_one %} subscription agreement.
|
||||
|
||||
@@ -216,8 +216,6 @@ c/o Corporation Service Company
|
||||
Sacramento, CA 95833-3505
|
||||
```
|
||||
|
||||
You may also send a courtesy copy to legal@support.github.com.
|
||||
|
||||
Please make your requests as specific and narrow as possible, including the following information:
|
||||
|
||||
- Full information about authority issuing the request for information
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Enums
|
||||
redirect_from:
|
||||
- /v4/enum
|
||||
- /v4/reference/enum
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Input objects
|
||||
redirect_from:
|
||||
- /v4/input_object
|
||||
- /v4/reference/input_object
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Interfaces
|
||||
redirect_from:
|
||||
- /v4/interface
|
||||
- /v4/reference/interface
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Mutations
|
||||
redirect_from:
|
||||
- /v4/mutation
|
||||
- /v4/reference/mutation
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Objects
|
||||
redirect_from:
|
||||
- /v4/object
|
||||
- /v4/reference/object
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
|
||||
@@ -3,6 +3,7 @@ title: Queries
|
||||
miniTocMaxHeadingLevel: 2
|
||||
redirect_from:
|
||||
- /v4/query
|
||||
- /v4/reference/query
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Scalars
|
||||
redirect_from:
|
||||
- /v4/scalar
|
||||
- /v4/reference/scalar
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Unions
|
||||
redirect_from:
|
||||
- /v4/union
|
||||
- /v4/reference/union
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
|
||||
@@ -19,4 +19,4 @@ You can choose which contributors are included in metrics and create teams of co
|
||||
|
||||
### Further reading
|
||||
- "[Metrics available with {% data variables.product.prodname_insights %}](/insights/exploring-your-usage-of-github-enterprise/metrics-available-with-github-insights)"
|
||||
- "[{% data variables.product.prodname_insights %} and data protection for your organization](/github/site-policy/github-insights-and-data-protection-for-your-organization)"
|
||||
- "<a href="/github/site-policy/github-insights-and-data-protection-for-your-organization" class="dotcom-only">{% data variables.product.prodname_insights %} and data protection for your organization</a>"
|
||||
@@ -108,4 +108,4 @@ To configure {% data variables.product.prodname_insights %} to connect to {% dat
|
||||
### Further reading
|
||||
|
||||
- "[Managing repositories](/insights/installing-and-configuring-github-insights/managing-repositories)"
|
||||
- "[{% data variables.product.prodname_insights %} and data protection for your organization](/github/site-policy/github-insights-and-data-protection-for-your-organization)"
|
||||
- "<a href="/github/site-policy/github-insights-and-data-protection-for-your-organization" class="dotcom-only">{% data variables.product.prodname_insights %} and data protection for your organization</a>"
|
||||
|
||||
@@ -7,6 +7,7 @@ redirect_from:
|
||||
versions:
|
||||
free-pro-team: '*'
|
||||
enterprise-server: '*'
|
||||
github-ae: '*'
|
||||
---
|
||||
|
||||
<div class="jumbotron libraries-jumbotron">
|
||||
|
||||
@@ -40,7 +40,7 @@ http(s)://<em>hostname</em>/
|
||||
{% if currentVersion == "github-ae@latest" or enterpriseServerVersions contains currentVersion %}
|
||||
### Authentication
|
||||
|
||||
Your {% data variables.product.product_name %} installation's API endpoints accept [the same authentication methods](/rest/overview/resources-in-the-rest-api#authentication) as the GitHub.com API. You can authenticate yourself with **[OAuth tokens](/apps/building-integrations/setting-up-and-registering-oauth-apps/)** (which can be created using the [Authorizations API](/rest/reference/oauth-authorizations#create-a-new-authorization)) or **[basic authentication](/rest/overview/resources-in-the-rest-api#basic-authentication)**. {% if enterpriseServerVersions contains currentVersion %}
|
||||
Your {% data variables.product.product_name %} installation's API endpoints accept [the same authentication methods](/rest/overview/resources-in-the-rest-api#authentication) as the GitHub.com API. You can authenticate yourself with **[OAuth tokens](/apps/building-integrations/setting-up-and-registering-oauth-apps/)** {% if enterpriseServerVersions contains currentVersion %}(which can be created using the [Authorizations API](/rest/reference/oauth-authorizations#create-a-new-authorization)) {% endif %}or **[basic authentication](/rest/overview/resources-in-the-rest-api#basic-authentication)**. {% if enterpriseServerVersions contains currentVersion %}
|
||||
OAuth tokens must have the `site_admin` [OAuth scope](/developers/apps/scopes-for-oauth-apps#available-scopes) when used with Enterprise-specific endpoints.{% endif %}
|
||||
|
||||
Enterprise administration API endpoints are only accessible to authenticated {% data variables.product.product_name %} site administrators{% if enterpriseServerVersions contains currentVersion %}, except for the [Management Console](#management-console) API, which requires the [Management Console password](/enterprise/admin/articles/accessing-the-management-console/){% endif %}.
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@2.21" or currentVersion == "github-ae@latest" %}
|
||||
Suspending a {% data variables.product.prodname_github_app %} installation is currently in beta and subject to change. Before you can suspend a {% data variables.product.prodname_github_app %}, the app owner must enable suspending installations for the app by opting-in to the beta. To opt-in to the suspending installations beta feature, see "[Activating beta features for apps](/developers/apps/activating-beta-features-for-apps)."
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% if breadcrumb[1].href == '' %}
|
||||
<span>{{breadcrumb[1].title}}</span>
|
||||
{% else %}
|
||||
<a title="{{ breadcrumb[0]}}: {{breadcrumb[1].title}}" href="/{{currentLanguage}}{{breadcrumb[1].href}}" class="d-inline-block {% if breadcrumb[1].href == currentPathWithoutLanguage %}text-gray-light{% endif %}">
|
||||
<a title="{{ breadcrumb[0]}}: {{breadcrumb[1].title}}" href="{{{breadcrumb[1].href}}" class="d-inline-block {% if breadcrumb[1].href == currentPath %}text-gray-light{% endif %}">
|
||||
{{breadcrumb[1].title}}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<ul class="sidebar-categories">
|
||||
{% for category in product[1].categories %}
|
||||
<li class="sidebar-category {% if breadcrumbs.category.href == category[1].href %}active{% endif %}">
|
||||
<a href="/{{currentLanguage}}{{category[1].href}}">{{ category[1].title }}</a>
|
||||
<a href="{{category[1].href}}">{{ category[1].title }}</a>
|
||||
<!-- some categories have topics with child articles -->
|
||||
{% if category[1].maptopics %}
|
||||
<ul class="sidebar-maptopics">
|
||||
{% for maptopic in category[1].maptopics %}
|
||||
<li class="sidebar-maptopic {% if breadcrumbs.maptopic.href == maptopic[1].href %}active{% endif %}">
|
||||
<a href="/{{currentLanguage}}{{maptopic[1].href}}">{{ maptopic[1].title }}</a>
|
||||
<a href="{{maptopic[1].href}}">{{ maptopic[1].title }}</a>
|
||||
<ul class="sidebar-articles">
|
||||
{% for article in maptopic[1].articles %}
|
||||
<li class="sidebar-article {% if currentPath == article[1].href %}active{% endif %}">
|
||||
<a href="/{{currentLanguage}}{{article[1].href}}">{{ article[1].title }}</a>
|
||||
<a href="{{article[1].href}}">{{ article[1].title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -23,7 +23,7 @@
|
||||
<ul class="sidebar-articles">
|
||||
{% for article in category[1].articles %}
|
||||
<li class="sidebar-article{% if currentPath == article[1].href %} active{% endif %}">
|
||||
<a href="/{{currentLanguage}}{{article[1].href}}">{{ article[1].title }}</a>
|
||||
<a href="{{article[1].href}}">{{ article[1].title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
{% include all-products-link %}
|
||||
<li title="{{product.title}}" class="sidebar-product mb-2">
|
||||
{% unless page.hidden %}
|
||||
<a href="/{{currentLanguage}}{{product.href}}" class="pl-4 pr-5 pb-1 f4">{{ product.title }}</a>
|
||||
<a href="{{product.href}}" class="pl-4 pr-5 pb-1 f4">{{ product.title }}</a>
|
||||
{% endunless %}
|
||||
</li>
|
||||
<ul class="sidebar-categories list-style-none">
|
||||
{% for category in product.categories %}
|
||||
{% capture fullPathToCategory %}/{{currentLanguage}}{{category[1].href}}{% endcapture %}
|
||||
{% capture fullPathToCategory %}{{category[1].href}}{% endcapture %}
|
||||
|
||||
<li class="sidebar-category py-1 {% if breadcrumbs.category.href == category[1].href %}active {% if currentPath == fullPathToCategory %}is-current-page {% endif %}{% endif %}{% if category[1].standalone %}standalone-category{% endif %}">
|
||||
{% if category[1].standalone %}
|
||||
@@ -37,13 +37,13 @@
|
||||
<ul class="sidebar-topics list-style-none position-relative">
|
||||
{% for maptopic in category[1].maptopics %}
|
||||
{% unless maptopic[1].hidden %}
|
||||
{% capture fullPathToMaptopic %}/{{currentLanguage}}{{maptopic[1].href}}{% endcapture %}
|
||||
{% capture fullPathToMaptopic %}{{maptopic[1].href}}{% endcapture %}
|
||||
|
||||
<li class="sidebar-maptopic {% if breadcrumbs.maptopic.href == maptopic[1].href %}active {% if currentPath == fullPathToMaptopic %}is-current-page{% endif %}{% endif %}">
|
||||
<a href="{{fullPathToMaptopic}}" class="pl-4 pr-5 py-2">{{ maptopic[1].title }}</a>
|
||||
<ul class="sidebar-articles my-2">
|
||||
{% for article in maptopic[1].articles %}
|
||||
{% capture fullPathToArticle %}/{{currentLanguage}}{{article[1].href}}{% endcapture %}
|
||||
{% capture fullPathToArticle %}{{article[1].href}}{% endcapture %}
|
||||
|
||||
<li class="sidebar-article {% if breadcrumbs.article.href == article[1].href %}active {% if currentPath == fullPathToArticle %}is-current-page{% endif %}{% endif %}">
|
||||
<a href="{{fullPathToArticle}}" class="pl-6 pr-5 py-1{% if forloop.last %} pb-2{% endif %}">{{ article[1].title }}</a>
|
||||
@@ -58,7 +58,7 @@
|
||||
{% else %}
|
||||
<ul class="sidebar-articles list-style-none">
|
||||
{% for article in category[1].articles %}
|
||||
{% capture fullPathToArticle %}/{{currentLanguage}}{{article[1].href}}{% endcapture %}
|
||||
{% capture fullPathToArticle %}{{article[1].href}}{% endcapture %}
|
||||
<li class="sidebar-article {% if breadcrumbs.article.href == article[1].href %}active {% if currentPath == fullPathToArticle %}is-current-page{% endif %}{% endif %}">
|
||||
<a href="{{fullPathToArticle}}" class="pl-4 pr-5 py-1{% if forloop.last %} pb-2{% endif %}">{{ article[1].title }}</a>
|
||||
</li>
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
const cheerio = require('cheerio')
|
||||
const findPageInVersion = require('./find-page-in-version')
|
||||
const renderContent = require('./render-content')
|
||||
const rewriteLocalLinks = require('./rewrite-local-links')
|
||||
const nonEnterpriseDefaultVersion = require('./non-enterprise-default-version')
|
||||
const { getPathWithoutLanguage } = require('./path-utils')
|
||||
const { getEnterpriseVersionNumber, adminProduct } = require('./patterns')
|
||||
const { deprecated, latest } = require('./enterprise-server-releases')
|
||||
|
||||
// internal links will have a language code by the time we're testing them
|
||||
// we also want to capture same-page anchors (#foo)
|
||||
const languageCode = 'en'
|
||||
const internalHrefs = ['/en', '#']
|
||||
|
||||
const renderedPageCache = {}
|
||||
const checkedAnchorCache = {}
|
||||
|
||||
module.exports = async function checkLinks ($, page, context, version, checkedLinkCache = {}) {
|
||||
// run rewriteLocalLinks to version links and add language codes
|
||||
rewriteLocalLinks($, version, languageCode)
|
||||
|
||||
const brokenLinks = {
|
||||
anchors: [],
|
||||
links: []
|
||||
}
|
||||
|
||||
// internal link check
|
||||
for (const href of internalHrefs) {
|
||||
const internalLinks = $(`a[href^="${href}"]`).get()
|
||||
|
||||
for (const internalLink of internalLinks) {
|
||||
const href = $(internalLink).attr('href')
|
||||
|
||||
// enable caching so we don't check links more than once
|
||||
// anchor links are cached locally (within this run) since they are specific to the page
|
||||
if (checkedLinkCache[href] || checkedAnchorCache[href]) continue
|
||||
|
||||
const [link, anchor] = href.split('#')
|
||||
|
||||
// if anchor only (e.g., #foo), look for heading on same page
|
||||
if (anchor && !link) {
|
||||
// ignore anchors that are autogenerated from headings
|
||||
if (anchor === $(internalLink).parent().attr('id')) continue
|
||||
|
||||
const matchingHeadings = getMatchingHeadings($, anchor)
|
||||
|
||||
if (matchingHeadings.length === 0) {
|
||||
brokenLinks.anchors.push({ 'broken same-page anchor': `#${anchor}`, reason: 'heading not found on page' })
|
||||
}
|
||||
checkedAnchorCache[href] = true
|
||||
continue
|
||||
}
|
||||
checkedLinkCache[href] = true
|
||||
|
||||
// skip rare hardcoded links to old GHE versions
|
||||
// these paths will always be in the old versioned form
|
||||
// example: /enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release
|
||||
const gheVersionInLink = link.match(getEnterpriseVersionNumber)
|
||||
if (gheVersionInLink && deprecated.includes(gheVersionInLink[1])) continue
|
||||
|
||||
// look for linked page
|
||||
const isDotcomOnly = $(internalLink).attr('class')
|
||||
|
||||
// special case for GHES Admin links on dotcom, which are not broken; they go to the latest GHES version
|
||||
let versionToCheck = version
|
||||
if (version === nonEnterpriseDefaultVersion && adminProduct.test(link)) {
|
||||
versionToCheck = `enterprise-server@${latest}`
|
||||
}
|
||||
|
||||
const linkedPage = findPageInVersion(link, context.pages, context.redirects, languageCode, versionToCheck, isDotcomOnly)
|
||||
|
||||
if (!linkedPage) {
|
||||
brokenLinks.links.push({ 'broken link': link, reason: 'linked page not found' })
|
||||
continue
|
||||
}
|
||||
|
||||
if (linkedPage.relativePath.includes('rest/reference/') && linkedPage.relativePath !== 'rest/reference/index.md') {
|
||||
const linkedPageRelevantPermalink = linkedPage.permalinks.find(permalink => permalink.pageVersion === version)
|
||||
if (!linkedPageRelevantPermalink) continue
|
||||
|
||||
const docsPath = linkedPageRelevantPermalink.href
|
||||
.split('rest/reference/')[1]
|
||||
.split('#')[0] // do not include #fragments
|
||||
|
||||
// find all operations that with an operationID that matches the requested docs path
|
||||
context.currentRestOperations = context.operationsForCurrentProduct
|
||||
.filter(operation => operation.operationId.startsWith(docsPath))
|
||||
}
|
||||
|
||||
// collect elements of the page that may contain links
|
||||
const linkedPageContent = linkedPage.relativePath.includes('graphql/reference/objects')
|
||||
? linkedPage.markdown + context.graphql.prerenderedObjectsForCurrentVersion.html
|
||||
: linkedPage.markdown
|
||||
|
||||
// create a unique string for caching purposes
|
||||
const pathToCache = version + linkedPage.relativePath
|
||||
|
||||
const anchorToCheck = anchor
|
||||
|
||||
// if link with anchor (e.g., /some/path#foo), look for heading on linked page
|
||||
if (anchorToCheck) {
|
||||
// either render page or fetch it from cache if we've already rendered it
|
||||
let linkedPageObject
|
||||
if (!renderedPageCache[pathToCache]) {
|
||||
const linkedPageHtml = await renderContent(linkedPageContent, context)
|
||||
linkedPageObject = cheerio.load(linkedPageHtml, { xmlMode: true })
|
||||
renderedPageCache[pathToCache] = linkedPageObject
|
||||
} else {
|
||||
linkedPageObject = renderedPageCache[pathToCache]
|
||||
}
|
||||
|
||||
const matchingHeadings = getMatchingHeadings(linkedPageObject, anchorToCheck)
|
||||
|
||||
if (matchingHeadings.length === 0) {
|
||||
if (anchor) {
|
||||
brokenLinks.anchors.push({ 'broken anchor': `#${anchor}`, 'full link': `${getPathWithoutLanguage(link)}#${anchor}`, reason: 'heading not found on linked page', 'linked page': linkedPage.fullPath })
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { brokenLinks, checkedLinkCache }
|
||||
}
|
||||
|
||||
// article titles are h1s; headings can be any subsequent level
|
||||
function getMatchingHeadings ($, anchor) {
|
||||
return $(`
|
||||
h2[id="${anchor}"],
|
||||
h3[id="${anchor}"],
|
||||
h4[id="${anchor}"],
|
||||
h5[id="${anchor}"],
|
||||
h6[id="${anchor}"],
|
||||
a[name="${anchor}"]
|
||||
`).get()
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const rewriteAssetPathsToS3 = require('./rewrite-asset-paths-to-s3')
|
||||
const { promisify } = require('util')
|
||||
|
||||
module.exports = async function checkImages ($, version, relativePath, checkedImageCache = {}) {
|
||||
rewriteAssetPathsToS3($, version, relativePath)
|
||||
|
||||
const brokenImages = []
|
||||
|
||||
// this does not check S3 images because those live outside of the repo
|
||||
const images = $('img[src^="/assets"]').get()
|
||||
|
||||
for (const image of images) {
|
||||
const src = $(image).attr('src')
|
||||
|
||||
if (checkedImageCache[src]) continue
|
||||
|
||||
try {
|
||||
await promisify(fs.access)(path.join(__dirname, '..', src))
|
||||
} catch (e) {
|
||||
brokenImages.push({ 'broken image reference': src })
|
||||
}
|
||||
}
|
||||
|
||||
return { brokenImages, checkedImageCache }
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
const cheerio = require('cheerio')
|
||||
const findPageInVersion = require('./find-page-in-version')
|
||||
const renderContent = require('./render-content')
|
||||
const rewriteLocalLinks = require('./rewrite-local-links')
|
||||
const nonEnterpriseDefaultVersion = require('./non-enterprise-default-version')
|
||||
const { getPathWithoutLanguage } = require('./path-utils')
|
||||
const { getEnterpriseVersionNumber, adminProduct } = require('./patterns')
|
||||
const { deprecated, latest } = require('./enterprise-server-releases')
|
||||
|
||||
// internal links will have a language code by the time we're testing them
|
||||
// we also want to capture same-page anchors (#foo)
|
||||
const languageCode = 'en'
|
||||
const internalHrefs = ['/en', '#']
|
||||
|
||||
const renderedPageCache = {}
|
||||
const checkedAnchorCache = {}
|
||||
|
||||
module.exports = async function checkLinks ($, page, context, version, checkedLinkCache = {}) {
|
||||
// run rewriteLocalLinks to version links and add language codes
|
||||
rewriteLocalLinks($, version, languageCode)
|
||||
|
||||
const brokenLinks = {
|
||||
anchors: [],
|
||||
links: []
|
||||
}
|
||||
|
||||
// internal link check
|
||||
for (const href of internalHrefs) {
|
||||
const internalLinks = $(`a[href^="${href}"]`).get()
|
||||
|
||||
for (const internalLink of internalLinks) {
|
||||
const href = $(internalLink).attr('href')
|
||||
|
||||
// enable caching so we don't check links more than once
|
||||
// anchor links are cached locally (within this run) since they are specific to the page
|
||||
if (checkedLinkCache[href] || checkedAnchorCache[href]) continue
|
||||
|
||||
const [link, anchor] = href.split('#')
|
||||
|
||||
// if anchor only (e.g., #foo), look for heading on same page
|
||||
if (anchor && !link) {
|
||||
// ignore anchors that are autogenerated from headings
|
||||
if (anchor === $(internalLink).parent().attr('id')) continue
|
||||
|
||||
const matchingHeadings = getMatchingHeadings($, anchor)
|
||||
|
||||
if (matchingHeadings.length === 0) {
|
||||
brokenLinks.anchors.push({ 'broken same-page anchor': `#${anchor}`, reason: 'heading not found on page' })
|
||||
}
|
||||
checkedAnchorCache[href] = true
|
||||
continue
|
||||
}
|
||||
checkedLinkCache[href] = true
|
||||
|
||||
// skip rare hardcoded links to old GHE versions
|
||||
// these paths will always be in the old versioned form
|
||||
// example: /enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release
|
||||
const gheVersionInLink = link.match(getEnterpriseVersionNumber)
|
||||
if (gheVersionInLink && deprecated.includes(gheVersionInLink[1])) continue
|
||||
|
||||
// look for linked page
|
||||
const isDotcomOnly = $(internalLink).attr('class')
|
||||
|
||||
// special case for GHES Admin links on dotcom, which are not broken; they go to the latest GHES version
|
||||
let versionToCheck = version
|
||||
if (version === nonEnterpriseDefaultVersion && adminProduct.test(link)) {
|
||||
versionToCheck = `enterprise-server@${latest}`
|
||||
}
|
||||
|
||||
const linkedPage = findPageInVersion(link, context.pages, context.redirects, languageCode, versionToCheck, isDotcomOnly)
|
||||
|
||||
if (!linkedPage) {
|
||||
brokenLinks.links.push({ 'broken link': link, reason: 'linked page not found' })
|
||||
continue
|
||||
}
|
||||
|
||||
// don't check anchors on developers content
|
||||
if (linkedPage.relativePath.match(/^(rest|graphql|developers)/)) continue
|
||||
|
||||
// create a unique string for caching purposes
|
||||
const pathToCache = version + linkedPage.relativePath
|
||||
|
||||
const anchorToCheck = anchor
|
||||
|
||||
// if link with anchor (e.g., /some/path#foo), look for heading on linked page
|
||||
if (anchorToCheck) {
|
||||
// either render page or fetch it from cache if we've already rendered it
|
||||
let linkedPageObject
|
||||
if (!renderedPageCache[pathToCache]) {
|
||||
const linkedPageHtml = await renderContent(linkedPage.markdown, context)
|
||||
linkedPageObject = cheerio.load(linkedPageHtml, { xmlMode: true })
|
||||
renderedPageCache[pathToCache] = linkedPageObject
|
||||
} else {
|
||||
linkedPageObject = renderedPageCache[pathToCache]
|
||||
}
|
||||
|
||||
const matchingHeadings = getMatchingHeadings(linkedPageObject, anchorToCheck)
|
||||
|
||||
if (matchingHeadings.length === 0) {
|
||||
if (anchor) {
|
||||
brokenLinks.anchors.push({ 'broken anchor': `#${anchor}`, 'full link': `${getPathWithoutLanguage(link)}#${anchor}`, reason: 'heading not found on linked page', 'linked page': linkedPage.fullPath })
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { brokenLinks, checkedLinkCache }
|
||||
}
|
||||
|
||||
// article titles are h1s; headings can be any subsequent level
|
||||
function getMatchingHeadings ($, anchor) {
|
||||
return $(`
|
||||
h2[id="${anchor}"],
|
||||
h3[id="${anchor}"],
|
||||
h4[id="${anchor}"],
|
||||
h5[id="${anchor}"],
|
||||
h6[id="${anchor}"],
|
||||
a[name="${anchor}"]
|
||||
`)
|
||||
}
|
||||
@@ -8,6 +8,10 @@ module.exports = function findPage (href, pageMap, redirects = {}, languageCode
|
||||
// remove trailing slash
|
||||
href = slash(href).replace(patterns.trailingSlash, '$1')
|
||||
|
||||
// do an initial lookup on the path as-is
|
||||
let page = pageMap[removeFragment(href)]
|
||||
if (page) return page
|
||||
|
||||
// check all potential versions
|
||||
const versionedPathsToCheck = [...new Set(allVersions.map(version => {
|
||||
return getVersionedPathWithLanguage(href, version, languageCode)
|
||||
@@ -22,8 +26,8 @@ module.exports = function findPage (href, pageMap, redirects = {}, languageCode
|
||||
// need to account for redirects again
|
||||
pathToPage = redirects[pathToPage] || pathToPage
|
||||
|
||||
// find the page
|
||||
const page = pageMap[removeFragment(pathToPage)]
|
||||
// try finding the page again
|
||||
page = pageMap[removeFragment(pathToPage)]
|
||||
|
||||
if (page) return page
|
||||
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
const findPage = require('./find-page')
|
||||
const { get, last } = require('lodash')
|
||||
const { getLanguageCode } = require('../lib/patterns')
|
||||
|
||||
// get the page.childArticles set on english map topics in lib/site-tree.js
|
||||
module.exports = function getMapTopicContent (page, pageMap, redirects) {
|
||||
const englishPage = page.languageCode !== 'en'
|
||||
? findPage(`/${page.relativePath.replace(/.md$/, '')}`, pageMap, redirects, 'en')
|
||||
: page
|
||||
// get the childArticles set on map topics in lib/site-tree.js
|
||||
module.exports = function getMapTopicContent (productId, siteTree, currentLanguage, currentVersion, currentPath) {
|
||||
const maptopicPath = currentPath
|
||||
const categoryPath = currentPath.replace(new RegExp(`/${last(currentPath.split('/'))}$`), '')
|
||||
const siteTreePath = getSiteTreePath(currentVersion, productId, categoryPath, maptopicPath)
|
||||
let childArticles = get(siteTree[currentLanguage], siteTreePath)
|
||||
|
||||
if (!englishPage) {
|
||||
console.error(`cannot find english page: ${page.fullPath}`)
|
||||
return
|
||||
// try falling back to English if needed
|
||||
if (!childArticles && currentLanguage !== 'en') {
|
||||
const englishCategoryPath = categoryPath.replace(getLanguageCode, '/en')
|
||||
const englishMaptopicPath = maptopicPath.replace(getLanguageCode, '/en')
|
||||
const englishSiteTreePath = getSiteTreePath(currentVersion, productId, englishCategoryPath, englishMaptopicPath)
|
||||
childArticles = get(siteTree.en, englishSiteTreePath)
|
||||
}
|
||||
|
||||
if (!englishPage.childArticles) {
|
||||
console.error(`error getting child articles on map topic: ${page.fullPath}`)
|
||||
return
|
||||
if (!childArticles) {
|
||||
console.error(`can't find child articles for map topic ${currentPath}`)
|
||||
return ''
|
||||
}
|
||||
|
||||
return englishPage.childArticles
|
||||
return childArticles
|
||||
.map(article => `{% link_with_intro /${article.href} %}`)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
function getSiteTreePath (version, productId, categoryPath, maptopicPath) {
|
||||
return [version, 'products', productId, 'categories', categoryPath, 'maptopics', maptopicPath, 'childArticles']
|
||||
}
|
||||
|
||||
@@ -157,7 +157,8 @@ class Page {
|
||||
this.shortTitle = await renderContent(this.shortTitle, context, { textOnly: true, encodeEntities: true })
|
||||
|
||||
let markdown = this.mapTopic
|
||||
? getMapTopicContent(this, context.pages, context.redirects)
|
||||
// get the map topic child articles from the siteTree
|
||||
? getMapTopicContent(this.parentProduct.id, context.siteTree, context.currentLanguage, context.currentVersion, context.currentPath)
|
||||
: this.markdown
|
||||
|
||||
// If the article is interactive parse the React!
|
||||
|
||||
@@ -1,42 +1,52 @@
|
||||
const slash = require('slash')
|
||||
const path = require('path')
|
||||
const patterns = require('./patterns')
|
||||
const { deprecated } = require('./enterprise-server-releases')
|
||||
const { deprecated, latest } = require('./enterprise-server-releases')
|
||||
const allProducts = require('./all-products')
|
||||
const allVersions = require('./all-versions')
|
||||
const supportedVersions = new Set(Object.keys(allVersions))
|
||||
const { getNewVersionedPath } = require('./old-versions-utils')
|
||||
|
||||
const supportedVersions = new Set(Object.keys(allVersions))
|
||||
|
||||
// construct appropriate versioned path for any given HREF
|
||||
// This function constructs an appropriate versioned path for any given HREF.
|
||||
// NOTE: this gets called by findPage and various other functions, and
|
||||
// has to return a proper versioned link given a wide variety of incoming
|
||||
// modern or legacy-formatted links, so it is somewhat overloaded. At some point
|
||||
// this could probably be broken up into separate functions to handle different incoming
|
||||
// paths. But it is currently optimized to handle lots of edge cases.
|
||||
function getVersionedPathWithoutLanguage (href, version) {
|
||||
// start clean without language code or trailing slash
|
||||
// Start clean without language code or trailing slash
|
||||
href = getPathWithoutLanguage(href.replace(patterns.trailingSlash, '$1'))
|
||||
|
||||
// if this is an old versioned path that includes a deprecated version, do not change!
|
||||
// If this is an old versioned path that includes a deprecated version, do not change!
|
||||
// example: /enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release
|
||||
const oldEnterpriseVersionNumber = href.match(patterns.getEnterpriseVersionNumber)
|
||||
if (oldEnterpriseVersionNumber && deprecated.includes(oldEnterpriseVersionNumber[1])) {
|
||||
return href
|
||||
}
|
||||
|
||||
// try to derive the current version from the path
|
||||
// Try to derive the current version from the path
|
||||
// example: enterprise-server@2.22 or free-pro-team@latest
|
||||
let versionFromPath = getVersionStringFromPath(href)
|
||||
|
||||
// if the version found is not a currently supported version...
|
||||
// If a supported version was found, add it to the path so we can go through the rest of the checks
|
||||
if (supportedVersions.has(versionFromPath)) {
|
||||
href = href.replace(href.split('/')[1], versionFromPath)
|
||||
}
|
||||
|
||||
// If a currently supported version was NOT found...
|
||||
let productObjectFromPath
|
||||
if (!supportedVersions.has(versionFromPath)) {
|
||||
// first check if the first segment is instead a current product;
|
||||
// First check if the segment is instead a current product;
|
||||
// example: /admin/foo or /desktop/foo
|
||||
productObjectFromPath = allProducts[versionFromPath]
|
||||
|
||||
// if so, add the first supported version for that product to the href
|
||||
// If so, add the first supported version for that product to the href
|
||||
// (this is just to get a path with all the expected segments; the version will be updated later if needed)
|
||||
if (productObjectFromPath) {
|
||||
href = path.join('/', productObjectFromPath.versions[0], href)
|
||||
versionFromPath = productObjectFromPath.versions[0]
|
||||
} else {
|
||||
// otherwise, this may be an old path that should be converted to new path;
|
||||
// Otherwise, this may be an old path that should be converted to new path;
|
||||
// OLD: /enterprise/2.22/admin/installation OR /enterprise/admin/installation
|
||||
// NEW: /enterprise-server@2.22/admin/installation
|
||||
href = getNewVersionedPath(href)
|
||||
@@ -44,58 +54,83 @@ function getVersionedPathWithoutLanguage (href, version) {
|
||||
}
|
||||
}
|
||||
|
||||
// if not previously found, derive the product object from the path (e.g., github or admin)
|
||||
// If not previously found, derive the product object from the path (e.g., github or admin)
|
||||
if (!productObjectFromPath) {
|
||||
productObjectFromPath = getProductObjectFromPath(href)
|
||||
}
|
||||
|
||||
// if the product's versions don't include the specified version, nothing to change!
|
||||
// If the product's versions don't include the specified version, nothing to change!
|
||||
if (productObjectFromPath && !productObjectFromPath.versions.includes(version)) {
|
||||
return slash(href)
|
||||
}
|
||||
|
||||
// update the version
|
||||
// Update the version and return the path
|
||||
return slash(href.replace(versionFromPath, version))
|
||||
}
|
||||
|
||||
// add language code
|
||||
// Add language code to a versioned path
|
||||
function getVersionedPathWithLanguage (href, version, languageCode) {
|
||||
return getPathWithLanguage(getVersionedPathWithoutLanguage(href, version), languageCode)
|
||||
}
|
||||
|
||||
// add the language to the given HREF
|
||||
// /en/articles/foo -> /articles/foo
|
||||
// Add the language to the given HREF
|
||||
// /articles/foo -> /en/articles/foo
|
||||
function getPathWithLanguage (href, languageCode) {
|
||||
return slash(path.posix.join('/', languageCode, getPathWithoutLanguage(href)))
|
||||
.replace(patterns.trailingSlash, '$1')
|
||||
}
|
||||
|
||||
// remove the language from the given HREF
|
||||
// /articles/foo -> /en/articles/foo
|
||||
// Remove the language from the given HREF
|
||||
// /en/articles/foo -> /articles/foo
|
||||
function getPathWithoutLanguage (href) {
|
||||
return slash(href.replace(patterns.hasLanguageCode, '/'))
|
||||
}
|
||||
|
||||
// Remove the version segment from the path
|
||||
function getPathWithoutVersion (href) {
|
||||
return href.replace(`/${getVersionStringFromPath(href)}`, '')
|
||||
}
|
||||
|
||||
// Return the version segment in a path
|
||||
function getVersionStringFromPath (href) {
|
||||
href = getPathWithoutLanguage(href)
|
||||
const versionString = href.split('/')[1]
|
||||
|
||||
return versionString || 'homepage'
|
||||
// Return immediately if this is a link to the homepage
|
||||
if (href === '/') {
|
||||
return 'homepage'
|
||||
}
|
||||
|
||||
// Check if the first segment is a supported version
|
||||
const versionFromPath = href.split('/')[1]
|
||||
|
||||
if (supportedVersions.has(versionFromPath)) {
|
||||
return versionFromPath
|
||||
}
|
||||
|
||||
// If the version segment is the latest enterprise-server release, return the latest release
|
||||
if (versionFromPath === 'enterprise-server@latest') {
|
||||
return `enterprise-server@${latest}`
|
||||
}
|
||||
|
||||
// If it's just a plan with no @release (e.g., `enterprise-server`), return the latest release
|
||||
const planObject = Object.values(allVersions).find(v => v.plan === versionFromPath)
|
||||
if (planObject) {
|
||||
return allVersions[planObject.latestVersion].version
|
||||
}
|
||||
|
||||
// Otherwise, return the first segment as-is, which may not be a real supported version,
|
||||
// but additional checks are done on this segment in getVersionedPathWithoutLanguage
|
||||
return versionFromPath
|
||||
}
|
||||
|
||||
// Return the corresponding object for the version segment in a path
|
||||
function getVersionObjectFromPath (href) {
|
||||
const versionId = getVersionStringFromPath(href)
|
||||
const version = allVersions[versionId]
|
||||
const versionFromPath = getVersionStringFromPath(href)
|
||||
|
||||
if (!version) throw new Error(`No version found for ${href}`)
|
||||
|
||||
return version
|
||||
return allVersions[versionFromPath]
|
||||
}
|
||||
|
||||
// Return the product segment from the path
|
||||
function getProductStringFromPath (href) {
|
||||
href = getPathWithoutLanguage(href)
|
||||
const productString = href.split('/')[2]
|
||||
@@ -103,10 +138,11 @@ function getProductStringFromPath (href) {
|
||||
return productString || 'homepage'
|
||||
}
|
||||
|
||||
// Return the corresponding object for the product segment in a path
|
||||
function getProductObjectFromPath (href) {
|
||||
const productId = getProductStringFromPath(href)
|
||||
// Return undefined if product id derived from path can't be found in allProducts
|
||||
return allProducts[productId]
|
||||
const productFromPath = getProductStringFromPath(href)
|
||||
|
||||
return allProducts[productFromPath]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const patterns = require('../patterns')
|
||||
const { getVersionedPathWithLanguage } = require('../path-utils')
|
||||
const supportedVersions = new Set(Object.keys(require('../all-versions')))
|
||||
const getOldPathsFromPermalink = require('./get-old-paths-from-permalink')
|
||||
|
||||
module.exports = function generateRedirectsForPermalinks (permalinks, redirectFrontmatter) {
|
||||
@@ -25,6 +26,12 @@ module.exports = function generateRedirectsForPermalinks (permalinks, redirectFr
|
||||
// remove trailing slashes (sometimes present in frontmatter)
|
||||
frontmatterOldPath = frontmatterOldPath.replace(patterns.trailingSlash, '$1')
|
||||
|
||||
// support hardcoded versions in redirect frontmatter
|
||||
if (supportedVersions.has(frontmatterOldPath.split('/')[1])) {
|
||||
redirects[frontmatterOldPath] = permalink.href
|
||||
redirects[`/en${frontmatterOldPath}`] = permalink.href
|
||||
}
|
||||
|
||||
// get the old path for the current permalink version
|
||||
const versionedFrontmatterOldPath = getVersionedPathWithLanguage(frontmatterOldPath, permalink.pageVersion, permalink.languageCode)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const nonEnterpriseDefaultVersion = require('../non-enterprise-default-version')
|
||||
|
||||
// This function runs at server warmup and precompiles possible redirect routes.
|
||||
// It outputs them in key-value pairs within a neat Javascript object: { oldPath: newPath }
|
||||
module.exports = async function precompileRedirects (pageList, pageMap) {
|
||||
module.exports = function precompileRedirects (pageList, pageMap) {
|
||||
const allRedirects = {}
|
||||
|
||||
// 1. CURRENT PAGES PERMALINKS AND FRONTMATTER
|
||||
|
||||
@@ -5,9 +5,9 @@ const languages = require('./languages')
|
||||
const dataDirectory = require('./data-directory')
|
||||
const encodeBracketedParentheticals = require('./encode-bracketed-parentheticals')
|
||||
|
||||
const loadSiteDataFromDir = async dir => ({
|
||||
const loadSiteDataFromDir = dir => ({
|
||||
site: {
|
||||
data: await dataDirectory(path.join(dir, 'data'), {
|
||||
data: dataDirectory(path.join(dir, 'data'), {
|
||||
preprocess: dataString =>
|
||||
encodeBracketedParentheticals(dataString.trimEnd()),
|
||||
ignorePatterns: [/README\.md$/]
|
||||
@@ -15,10 +15,10 @@ const loadSiteDataFromDir = async dir => ({
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = async function loadSiteData () {
|
||||
module.exports = function loadSiteData () {
|
||||
// load english site data
|
||||
const siteData = {
|
||||
en: await loadSiteDataFromDir(languages.en.dir)
|
||||
en: loadSiteDataFromDir(languages.en.dir)
|
||||
}
|
||||
|
||||
// load and add other language data to siteData where keys match english keys,
|
||||
@@ -26,7 +26,7 @@ module.exports = async function loadSiteData () {
|
||||
const englishKeys = Object.keys(flat(siteData.en))
|
||||
for (const language of Object.values(languages)) {
|
||||
if (language.code === 'en') continue
|
||||
const data = await loadSiteDataFromDir(language.dir)
|
||||
const data = loadSiteDataFromDir(language.dir)
|
||||
for (const key of englishKeys) {
|
||||
set(
|
||||
siteData,
|
||||
@@ -45,5 +45,5 @@ module.exports = async function loadSiteData () {
|
||||
)
|
||||
}
|
||||
|
||||
return Promise.resolve(siteData)
|
||||
return siteData
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const path = require('path')
|
||||
const findPageInVersion = require('./find-page-in-version')
|
||||
const products = Object.values(require('../lib/all-products'))
|
||||
const { getVersionedPathWithoutLanguage } = require('./path-utils')
|
||||
const { getVersionedPathWithLanguage, getPathWithLanguage } = require('./path-utils')
|
||||
const languageCodes = Object.keys(require('./languages'))
|
||||
const addTitlesToTree = require('./site-tree-titles')
|
||||
const allVersions = Object.keys(require('./all-versions'))
|
||||
@@ -35,7 +35,8 @@ module.exports = async function buildSiteTree (pageMap, site, redirects) {
|
||||
return
|
||||
}
|
||||
|
||||
product.href = item.href
|
||||
// we don't want versioned product links because these links already have a default version in them
|
||||
product.href = getPathWithLanguage(item.href, languageCode)
|
||||
|
||||
// find the product TOC page and get TOC items
|
||||
const page = findPageInVersion(item.href, pageMap, redirects, languageCode, version)
|
||||
@@ -43,7 +44,7 @@ module.exports = async function buildSiteTree (pageMap, site, redirects) {
|
||||
// skip if page can't be found in this version
|
||||
if (!page) return
|
||||
|
||||
product.categories = buildCategoriesTree(page.tocItems, item.href, pageMap, redirects, version, languageCode)
|
||||
product.categories = buildCategoriesTree(page.tocItems, product.href, pageMap, redirects, version, languageCode)
|
||||
|
||||
productTree[item.id] = product
|
||||
return null
|
||||
@@ -67,11 +68,12 @@ function buildCategoriesTree (tocItems, productHref, pageMap, redirects, version
|
||||
|
||||
const categoryHref = path.join(productHref, item.href)
|
||||
|
||||
const versionedCategoryHref = getVersionedPathWithoutLanguage(categoryHref, version)
|
||||
// we DO want versioned category links
|
||||
const versionedCategoryHref = getVersionedPathWithLanguage(categoryHref, version, languageCode)
|
||||
category.href = versionedCategoryHref
|
||||
|
||||
// find the category TOC page and get its TOC items
|
||||
const page = findPageInVersion(categoryHref, pageMap, redirects, languageCode, version)
|
||||
const page = findPageInVersion(versionedCategoryHref, pageMap, redirects, languageCode, version)
|
||||
|
||||
// skip if page can't be found in this version
|
||||
if (!page) return
|
||||
@@ -90,9 +92,9 @@ function buildCategoriesTree (tocItems, productHref, pageMap, redirects, version
|
||||
// if TOC contains maptopics, build a maptopics tree
|
||||
// otherwise build an articles tree
|
||||
if (hasMaptopics) {
|
||||
category.maptopics = buildMaptopicsTree(page.tocItems, categoryHref, pageMap, redirects, version, languageCode)
|
||||
category.maptopics = buildMaptopicsTree(page.tocItems, versionedCategoryHref, pageMap, redirects, version, languageCode)
|
||||
} else {
|
||||
category.articles = buildArticlesTree(page.tocItems, categoryHref, pageMap, redirects, version, languageCode)
|
||||
category.articles = buildArticlesTree(page.tocItems, versionedCategoryHref, pageMap, redirects, version, languageCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +104,7 @@ function buildCategoriesTree (tocItems, productHref, pageMap, redirects, version
|
||||
return categoryTree
|
||||
}
|
||||
|
||||
function buildMaptopicsTree (tocItems, categoryHref, pageMap, redirects, version, languageCode) {
|
||||
function buildMaptopicsTree (tocItems, versionedCategoryHref, pageMap, redirects, version, languageCode) {
|
||||
const maptopicTree = {}
|
||||
|
||||
// for every maptopic in a category TOC...
|
||||
@@ -111,14 +113,11 @@ function buildMaptopicsTree (tocItems, categoryHref, pageMap, redirects, version
|
||||
.forEach(item => {
|
||||
const maptopic = {}
|
||||
|
||||
const maptopicHref = path.join(categoryHref, item.href)
|
||||
|
||||
const versionedMaptopicHref = getVersionedPathWithoutLanguage(maptopicHref, version)
|
||||
const versionedMaptopicHref = path.join(versionedCategoryHref, item.href)
|
||||
maptopic.href = versionedMaptopicHref
|
||||
|
||||
// we already have access to the child articles via the category TOC items
|
||||
// but we still need the page to get the available versions
|
||||
const page = findPageInVersion(maptopicHref, pageMap, redirects, languageCode, version)
|
||||
// find the category TOC page and get its TOC items
|
||||
const page = findPageInVersion(versionedMaptopicHref, pageMap, redirects, languageCode, version)
|
||||
|
||||
// skip if page can't be found in this version
|
||||
if (!page) return
|
||||
@@ -126,23 +125,20 @@ function buildMaptopicsTree (tocItems, categoryHref, pageMap, redirects, version
|
||||
// if this is not a maptopic, return early
|
||||
if (!page.mapTopic) return
|
||||
|
||||
const childArticles = getChildArticles(tocItems, item.href)
|
||||
|
||||
maptopic.title = page.title
|
||||
maptopic.shortTitle = page.shortTitle
|
||||
maptopic.hidden = page.hidden
|
||||
|
||||
// make the child articles accessible to the page object for maptopic rendering
|
||||
if (!page.childArticles) page.childArticles = childArticles
|
||||
maptopic.childArticles = getChildArticles(tocItems, item.href)
|
||||
maptopic.articles = buildArticlesTree(maptopic.childArticles, versionedCategoryHref, pageMap, redirects, version, languageCode)
|
||||
|
||||
maptopic.articles = buildArticlesTree(childArticles, categoryHref, pageMap, redirects, version, languageCode)
|
||||
maptopicTree[versionedMaptopicHref] = maptopic
|
||||
})
|
||||
|
||||
return maptopicTree
|
||||
}
|
||||
|
||||
function buildArticlesTree (tocItems, categoryHref, pageMap, redirects, version, languageCode) {
|
||||
function buildArticlesTree (tocItems, versionedCategoryHref, pageMap, redirects, version, languageCode) {
|
||||
const articleTree = {}
|
||||
|
||||
// REST categories may not have TOC items
|
||||
@@ -152,12 +148,11 @@ function buildArticlesTree (tocItems, categoryHref, pageMap, redirects, version,
|
||||
tocItems.forEach(item => {
|
||||
const article = {}
|
||||
|
||||
const articleHref = path.join(categoryHref, item.href)
|
||||
|
||||
const versionedArticleHref = getVersionedPathWithoutLanguage(articleHref, version)
|
||||
const versionedArticleHref = path.join(versionedCategoryHref, item.href)
|
||||
article.href = versionedArticleHref
|
||||
|
||||
const page = findPageInVersion(articleHref, pageMap, redirects, languageCode, version)
|
||||
// find the category TOC page and get its TOC items
|
||||
const page = findPageInVersion(versionedArticleHref, pageMap, redirects, languageCode, version)
|
||||
|
||||
// skip if page can't be found in this version
|
||||
if (!page) return
|
||||
|
||||
@@ -9,7 +9,7 @@ const loadSiteTree = require('./site-tree')
|
||||
const dog = {
|
||||
loadPages: statsd.timer(loadPages, 'load_pages'),
|
||||
loadPageMap: statsd.timer(loadPageMap, 'load_page_map'),
|
||||
loadRedirects: statsd.asyncTimer(loadRedirects, 'load_redirects'),
|
||||
loadRedirects: statsd.timer(loadRedirects, 'load_redirects'),
|
||||
loadSiteData: statsd.timer(loadSiteData, 'load_site_data'),
|
||||
loadSiteTree: statsd.asyncTimer(loadSiteTree, 'load_site_tree')
|
||||
}
|
||||
@@ -39,20 +39,20 @@ async function warmServer () {
|
||||
console.log('Priming context information...')
|
||||
}
|
||||
|
||||
if (!pageList || !site) {
|
||||
// Promise.all is used to load multiple things in parallel
|
||||
[pageList, site] = await Promise.all([
|
||||
pageList || dog.loadPages(),
|
||||
site || dog.loadSiteData()
|
||||
])
|
||||
if (!pageList) {
|
||||
pageList = dog.loadPages()
|
||||
}
|
||||
|
||||
if (!site) {
|
||||
site = dog.loadSiteData()
|
||||
}
|
||||
|
||||
if (!pageMap) {
|
||||
pageMap = await dog.loadPageMap(pageList)
|
||||
pageMap = dog.loadPageMap(pageList)
|
||||
}
|
||||
|
||||
if (!redirects) {
|
||||
redirects = await dog.loadRedirects(pageList, pageMap)
|
||||
redirects = dog.loadRedirects(pageList, pageMap)
|
||||
}
|
||||
|
||||
if (!siteTree) {
|
||||
|
||||
@@ -25,7 +25,7 @@ module.exports = async (req, res, next) => {
|
||||
res.set('content-type', r.headers['content-type'])
|
||||
res.set('content-length', r.headers['content-length'])
|
||||
res.set('x-is-archived', 'true')
|
||||
res.set('x-robots-tag', 'none')
|
||||
res.set('x-robots-tag', 'noindex')
|
||||
res.send(r.body)
|
||||
} catch (err) {
|
||||
next()
|
||||
|
||||
@@ -36,7 +36,7 @@ module.exports = async (req, res, next) => {
|
||||
try {
|
||||
const r = await got(getProxyPath(req.path, requestedVersion))
|
||||
res.set('content-type', r.headers['content-type'])
|
||||
res.set('x-robots-tag', 'none')
|
||||
res.set('x-robots-tag', 'noindex')
|
||||
|
||||
// make the stubbed redirect files added in >=2.18 return 301 instead of 200
|
||||
const staticRedirect = r.body.match(patterns.staticRedirect)
|
||||
|
||||
40
middleware/block-robots.js
Normal file
40
middleware/block-robots.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const languages = require('../lib/languages')
|
||||
const products = require('../lib/all-products')
|
||||
const { deprecated } = require('../lib/enterprise-server-releases.js')
|
||||
|
||||
const pathRegExps = [
|
||||
// Disallow indexing of WIP localized content
|
||||
...Object.values(languages)
|
||||
.filter(language => language.wip)
|
||||
.map(language => new RegExp(`^/${language.code}(/.*)?$`, 'i')),
|
||||
|
||||
// Disallow indexing of WIP products
|
||||
...Object.values(products)
|
||||
.filter(product => product.wip || product.hidden)
|
||||
.map(product => [
|
||||
new RegExp(`^/.*?${product.href}`, 'i'),
|
||||
...product.versions.map(
|
||||
version => new RegExp(`^/.*?${version}/${product.id}`, 'i')
|
||||
)
|
||||
]),
|
||||
|
||||
// Disallow indexing of deprecated enterprise versions
|
||||
...deprecated
|
||||
.map(version => [
|
||||
new RegExp(`^/.*?/enterprise-server@${version}/.*?`, 'i'),
|
||||
new RegExp(`^/.*?/enterprise/${version}/.*?`, 'i')
|
||||
])
|
||||
].flat()
|
||||
|
||||
function blockIndex (path) {
|
||||
return pathRegExps.some(pathRe => pathRe.test(path))
|
||||
}
|
||||
|
||||
const middleware = (req, res, next) => {
|
||||
if (blockIndex(req.path)) res.set('x-robots-tag', 'noindex')
|
||||
return next()
|
||||
}
|
||||
|
||||
middleware.blockIndex = blockIndex
|
||||
|
||||
module.exports = middleware
|
||||
@@ -27,7 +27,7 @@ module.exports = async (req, res, next) => {
|
||||
}
|
||||
|
||||
req.context.breadcrumbs.product = {
|
||||
href: path.posix.join('/', req.context.currentVersion, productPath),
|
||||
href: path.posix.join('/', req.context.currentLanguage, req.context.currentVersion, productPath),
|
||||
title: product.title
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ module.exports = async (req, res, next) => {
|
||||
// get category path
|
||||
// e.g., `getting-started-with-github` in /free-pro-team@latest/github/getting-started-with-github
|
||||
// or /enterprise-server@2.21/github/getting-started-with-github
|
||||
const categoryPath = path.posix.join('/', req.context.currentVersion, productPath, pathParts[1])
|
||||
const categoryPath = path.posix.join('/', req.context.currentLanguage, req.context.currentVersion, productPath, pathParts[1])
|
||||
|
||||
const category = product.categories[categoryPath]
|
||||
|
||||
@@ -53,7 +53,7 @@ module.exports = async (req, res, next) => {
|
||||
// e.g., /github/getting-started-with-github/learning-about-github
|
||||
let maptopic
|
||||
if (req.context.page.mapTopic) {
|
||||
const maptopicPath = path.posix.join(categoryPath, pathParts[2])
|
||||
const maptopicPath = req.path
|
||||
|
||||
maptopic = category.maptopics[maptopicPath]
|
||||
|
||||
@@ -64,9 +64,7 @@ module.exports = async (req, res, next) => {
|
||||
title: maptopic.shortTitle || maptopic.title
|
||||
}
|
||||
} else {
|
||||
// get article path
|
||||
// e.g., /github/getting-started-with-github/githubs-products
|
||||
const articlePath = path.posix.join(categoryPath, pathParts[2])
|
||||
const articlePath = req.path
|
||||
|
||||
// find parent maptopic if one exists
|
||||
// some categories don't have maptopics, e.g. site-policy
|
||||
@@ -81,17 +79,7 @@ module.exports = async (req, res, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
let articleKey = '/' + req.language + articlePath
|
||||
let articlePage = req.context.pages[articleKey]
|
||||
|
||||
// fall back to English if localized article does not exist
|
||||
if (!articlePage && req.language !== 'en') {
|
||||
articleKey = '/en' + articlePath
|
||||
articlePage = req.context.pages[articleKey]
|
||||
}
|
||||
|
||||
if (!articlePage) return next()
|
||||
|
||||
const articlePage = req.context.page
|
||||
const articleTitle = await articlePage.renderProp('shortTitle', req.context, { textOnly: true, encodeEntities: true })
|
||||
|
||||
req.context.breadcrumbs.article = {
|
||||
|
||||
@@ -40,8 +40,8 @@ module.exports = async (req, res, next) => {
|
||||
|
||||
// get Early Access category path
|
||||
// e.g., `enforcing-best-practices-with-github-policies` in /free-pro-team@latest/early-access/github/enforcing-best-practices-with-github-policies
|
||||
const categoryPath = path.posix.join('/', req.context.currentVersion, 'early-access', pathParts[0], pathParts[1])
|
||||
const category = req.context.pages[path.posix.join('/en', categoryPath)]
|
||||
const categoryPath = path.posix.join('/', 'en', req.context.currentVersion, 'early-access', pathParts[0], pathParts[1])
|
||||
const category = req.context.pages[categoryPath]
|
||||
|
||||
if (!category) return next()
|
||||
|
||||
@@ -54,7 +54,7 @@ module.exports = async (req, res, next) => {
|
||||
|
||||
// for Early Access purposes, we don't need to differentiate between map topics and articles breadcrumbs
|
||||
const mapTopicOrArticlePath = path.posix.join(categoryPath, pathParts[2])
|
||||
const mapTopicOrArticle = req.context.pages[path.posix.join('/en', mapTopicOrArticlePath)]
|
||||
const mapTopicOrArticle = req.context.pages[mapTopicOrArticlePath]
|
||||
|
||||
if (!mapTopicOrArticle) return next()
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ module.exports = function (app) {
|
||||
|
||||
// *** Config and context for rendering ***
|
||||
app.use(require('./find-page')) // Must come before archived-enterprise-versions, breadcrumbs, featured-links, products, render-page
|
||||
app.use(require('./block-robots'))
|
||||
|
||||
// *** Rendering, 2xx responses ***
|
||||
// I largely ordered these by use frequency
|
||||
|
||||
@@ -1,33 +1,4 @@
|
||||
const languages = require('../lib/languages')
|
||||
const products = require('../lib/all-products')
|
||||
const { deprecated } = require('../lib/enterprise-server-releases.js')
|
||||
|
||||
let defaultResponse = 'User-agent: *'
|
||||
|
||||
// Disallow crawling of WIP localized content
|
||||
Object.values(languages)
|
||||
.filter(language => language.wip)
|
||||
.forEach(language => {
|
||||
defaultResponse = defaultResponse.concat(`\nDisallow: /${language.code}\nDisallow: /${language.code}/*\n`)
|
||||
})
|
||||
|
||||
// Disallow crawling of WIP products
|
||||
Object.values(products)
|
||||
.filter(product => product.wip || product.hidden)
|
||||
.forEach(product => {
|
||||
defaultResponse = defaultResponse.concat(`\nDisallow: /*${product.href}`)
|
||||
product.versions.forEach(version => {
|
||||
defaultResponse = defaultResponse.concat(`\nDisallow: /*${version}/${product.id}`)
|
||||
})
|
||||
})
|
||||
|
||||
// Disallow crawling of Deprecated enterprise versions
|
||||
deprecated
|
||||
.forEach(version => {
|
||||
defaultResponse = defaultResponse
|
||||
.concat(`\nDisallow: /*/enterprise-server@${version}/*`)
|
||||
.concat(`\nDisallow: /*/enterprise/${version}/*`)
|
||||
})
|
||||
const defaultResponse = 'User-agent: *'
|
||||
|
||||
const disallowAll = `User-agent: *
|
||||
Disallow: /`
|
||||
|
||||
@@ -7,6 +7,7 @@ const GithubSlugger = require('github-slugger')
|
||||
const { XmlEntities } = require('html-entities')
|
||||
const loadSiteData = require('../../lib/site-data')
|
||||
const renderContent = require('../../lib/render-content')
|
||||
const getApplicableVersions = require('../../lib/get-applicable-versions')
|
||||
|
||||
const slugger = new GithubSlugger()
|
||||
const entities = new XmlEntities()
|
||||
@@ -25,7 +26,7 @@ describe('category pages', () => {
|
||||
|
||||
const walkOptions = {
|
||||
globs: ['*/index.md', 'enterprise/*/index.md'],
|
||||
ignore: ['{rest,graphql,developers}/**', 'enterprise/index.md', '**/articles/**', 'early-access/**'],
|
||||
ignore: ['{rest,graphql}/**', 'enterprise/index.md', '**/articles/**', 'early-access/**'],
|
||||
directories: false,
|
||||
includeBasePath: true
|
||||
}
|
||||
@@ -64,7 +65,8 @@ describe('category pages', () => {
|
||||
describe.each(categoryTuples)(
|
||||
'category index "%s"',
|
||||
(indexRelPath, indexAbsPath, indexLink) => {
|
||||
let publishedArticlePaths, availableArticlePaths, indexTitle
|
||||
let publishedArticlePaths, availableArticlePaths, indexTitle, categoryVersions
|
||||
const articleVersions = {}
|
||||
|
||||
beforeAll(async () => {
|
||||
const categoryDir = path.dirname(indexAbsPath)
|
||||
@@ -72,6 +74,7 @@ describe('category pages', () => {
|
||||
// Get child article links included in each subdir's index page
|
||||
const indexContents = await fs.promises.readFile(indexAbsPath, 'utf8')
|
||||
const { data, content } = matter(indexContents)
|
||||
categoryVersions = getApplicableVersions(data.versions, indexAbsPath)
|
||||
const articleLinks = getLinks(content)
|
||||
|
||||
// Save the index title for later testing
|
||||
@@ -108,6 +111,15 @@ describe('category pages', () => {
|
||||
return `/${path.relative(categoryDir, articlePath).replace(/\.md$/, '')}`
|
||||
})
|
||||
)).filter(Boolean)
|
||||
|
||||
await Promise.all(
|
||||
childFilePaths.map(async (articlePath) => {
|
||||
const articleContents = await fs.promises.readFile(articlePath, 'utf8')
|
||||
const { data } = matter(articleContents)
|
||||
|
||||
articleVersions[articlePath] = getApplicableVersions(data.versions, articlePath)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('contains all expected articles', () => {
|
||||
@@ -122,6 +134,14 @@ describe('category pages', () => {
|
||||
expect(unexpectedArticles.length, errorMessage).toBe(0)
|
||||
})
|
||||
|
||||
test('contains only articles and map topics with versions that are also available in the parent category', () => {
|
||||
Object.entries(articleVersions).forEach(([articleName, articleVersions]) => {
|
||||
const unexpectedVersions = difference(articleVersions, categoryVersions)
|
||||
const errorMessage = `${articleName} has versions that are not available in parent category`
|
||||
expect(unexpectedVersions.length, errorMessage).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: Unskip this test once the related script has been executed
|
||||
test.skip('slugified title matches parent directory name', () => {
|
||||
// Get the parent directory name
|
||||
|
||||
@@ -24,23 +24,23 @@ describe('siteTree', () => {
|
||||
test('object order', () => {
|
||||
expect(Object.keys(siteTree)[0]).toBe('en')
|
||||
expect(Object.keys(siteTree.en)[0]).toBe(nonEnterpriseDefaultVersion)
|
||||
expect(Object.keys(siteTree.en[nonEnterpriseDefaultVersion].products.github.categories)[0]).toBe(`/${nonEnterpriseDefaultVersion}/github/getting-started-with-github`)
|
||||
expect(Object.keys(siteTree.en[nonEnterpriseDefaultVersion].products.github.categories)[0]).toBe(`/en/${nonEnterpriseDefaultVersion}/github/getting-started-with-github`)
|
||||
})
|
||||
|
||||
test('object structure', () => {
|
||||
expect(nonEnterpriseDefaultVersion in siteTree.en).toBe(true)
|
||||
expect(`enterprise-server@${latestEnterpriseRelease}` in siteTree.en).toBe(true)
|
||||
expect(flatTree[`en.${nonEnterpriseDefaultVersion}.products.github.href`]).toBe(`/${nonEnterpriseDefaultVersion}/github`)
|
||||
expect(flatTree[`en.${nonEnterpriseDefaultVersion}.products.github.categories./${nonEnterpriseDefaultVersion}/github/getting-started-with-github.href`]).toBe(`/${nonEnterpriseDefaultVersion}/github/getting-started-with-github`)
|
||||
expect(flatTree[`en.${nonEnterpriseDefaultVersion}.products.github.href`]).toBe(`/en/${nonEnterpriseDefaultVersion}/github`)
|
||||
expect(flatTree[`en.${nonEnterpriseDefaultVersion}.products.github.categories./en/${nonEnterpriseDefaultVersion}/github/getting-started-with-github.href`]).toBe(`/en/${nonEnterpriseDefaultVersion}/github/getting-started-with-github`)
|
||||
})
|
||||
|
||||
describe('localized titles', () => {
|
||||
test('titles for categories', () => {
|
||||
const japaneseTitle = flatTree[`ja.${nonEnterpriseDefaultVersion}.products.github.categories./${nonEnterpriseDefaultVersion}/github/getting-started-with-github.title`]
|
||||
const japaneseTitle = flatTree[`ja.${nonEnterpriseDefaultVersion}.products.github.categories./ja/${nonEnterpriseDefaultVersion}/github/getting-started-with-github.title`]
|
||||
expect(typeof japaneseTitle).toBe('string')
|
||||
expect(japaneseCharacters.presentIn(japaneseTitle)).toBe(true)
|
||||
|
||||
const englishTitle = flatTree[`en.${nonEnterpriseDefaultVersion}.products.github.categories./${nonEnterpriseDefaultVersion}/github/getting-started-with-github.title`]
|
||||
const englishTitle = flatTree[`en.${nonEnterpriseDefaultVersion}.products.github.categories./en/${nonEnterpriseDefaultVersion}/github/getting-started-with-github.title`]
|
||||
expect(typeof englishTitle).toBe('string')
|
||||
expect(japaneseCharacters.presentIn(englishTitle)).toBe(false)
|
||||
})
|
||||
@@ -52,7 +52,7 @@ describe('siteTree', () => {
|
||||
test('articles that include site data in liquid templating', () => {
|
||||
const pageWithDynamicTitle = siteTree.en[`enterprise-server@${latestEnterpriseRelease}`]
|
||||
.products.admin
|
||||
.categories[`/enterprise-server@${latestEnterpriseRelease}/admin/enterprise-support`]
|
||||
.categories[`/en/enterprise-server@${latestEnterpriseRelease}/admin/enterprise-support`]
|
||||
// Source frontmatter from article:
|
||||
// title: 'Working with {{ site.data.variables.contact.github_support }}'
|
||||
expect(pageWithDynamicTitle.title).toEqual('Working with GitHub Support')
|
||||
|
||||
271
tests/helpers/links-checker.js
Normal file
271
tests/helpers/links-checker.js
Normal file
@@ -0,0 +1,271 @@
|
||||
const cheerio = require('cheerio')
|
||||
const { union, uniq } = require('lodash')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const { getVersionStringFromPath } = require('../../lib/path-utils')
|
||||
const patterns = require('../../lib/patterns')
|
||||
const { deprecated } = require('../../lib/enterprise-server-releases')
|
||||
const findPageInVersion = require('../../lib/find-page-in-version')
|
||||
const rest = require('../../middleware/contextualizers/rest')
|
||||
const graphql = require('../../middleware/contextualizers/graphql')
|
||||
const contextualize = require('../../middleware/context')
|
||||
const releaseNotes = require('../../middleware/contextualizers/enterprise-release-notes')
|
||||
const versionSatisfiesRange = require('../../lib/version-satisfies-range')
|
||||
|
||||
class LinksChecker {
|
||||
constructor (opts = { languageCode: 'en', internalHrefPrefixes: ['/', '#'] }) {
|
||||
Object.assign(this, { ...opts })
|
||||
|
||||
// Some caching mechanism so we do not load pages unnecessarily,
|
||||
// nor check links that have been checked
|
||||
this.pageCache = new Map()
|
||||
this.checkedLinksCache = new Set()
|
||||
|
||||
// stores images to check all at once in a Map:
|
||||
// imageSrc => {
|
||||
// "usedBy": [version:path, ...]
|
||||
// }
|
||||
this.imagesToCheck = new Map()
|
||||
|
||||
// Stores broken images in a Map, formatted the same way as imagesToCheck
|
||||
this.brokenImages = new Map()
|
||||
|
||||
// Stores broken links in a Map in the format of:
|
||||
// link => {
|
||||
// linkedFrom: [ version:filePath, ... ]
|
||||
// }, ...
|
||||
this.brokenLinks = new Map()
|
||||
|
||||
// stores anchor links to check all at once in a Map:
|
||||
// version:filePath => {
|
||||
// '#anchor-link' : {
|
||||
// linkedFrom: ['url1', 'url2']
|
||||
// },
|
||||
// '#anchor-link2': {...}
|
||||
// }
|
||||
this.anchorLinksToCheck = new Map()
|
||||
|
||||
// Stores broken anchors in a Map, formatted the same way as anchorLinksToCheck
|
||||
this.brokenAnchors = new Map()
|
||||
}
|
||||
|
||||
async setRenderedPageObj (pathCacheKey, context, reRender = false) {
|
||||
if (this.pageCache.has(pathCacheKey) && !reRender) return
|
||||
let pageHTML = await context.page.render(context)
|
||||
|
||||
// handle special pre-rendered snowflake
|
||||
if (context.page.relativePath.endsWith('graphql/reference/objects.md')) {
|
||||
pageHTML += context.graphql.prerenderedObjectsForCurrentVersion.html
|
||||
}
|
||||
|
||||
const pageObj = cheerio.load(pageHTML, { xmlMode: true })
|
||||
this.pageCache.set(pathCacheKey, pageObj)
|
||||
}
|
||||
|
||||
async getRenderedPageObj (pathCacheKey, context) {
|
||||
if (!this.pageCache.has(pathCacheKey)) {
|
||||
if (context) {
|
||||
await this.setRenderedPageObj(pathCacheKey, context)
|
||||
} else {
|
||||
console.error('cannot find pre-rendered page, and does not have enough context to render one.')
|
||||
}
|
||||
}
|
||||
return this.pageCache.get(pathCacheKey)
|
||||
}
|
||||
|
||||
addAnchorForLater (pagePath, anchor, linkedFrom) {
|
||||
const anchorsInPath = this.anchorLinksToCheck.get(pagePath) || {}
|
||||
const anchorLink = anchorsInPath[anchor] || { linkedFrom: [] }
|
||||
anchorLink.linkedFrom = union(anchorLink.linkedFrom, [linkedFrom])
|
||||
anchorsInPath[anchor] = anchorLink
|
||||
this.anchorLinksToCheck.set(pagePath, anchorsInPath)
|
||||
}
|
||||
|
||||
addImagesForLater (images, pagePath) {
|
||||
uniq(images).forEach(imageSrc => {
|
||||
const imageUsage = this.imagesToCheck.get(imageSrc) || { usedBy: [] }
|
||||
imageUsage.usedBy = union(imageUsage.usedBy, [pagePath])
|
||||
this.imagesToCheck.set(imageSrc, imageUsage)
|
||||
})
|
||||
}
|
||||
|
||||
async checkPage (context, checkExternalAnchors) {
|
||||
const path = context.relativePath
|
||||
const version = context.currentVersion
|
||||
|
||||
const pathCacheKey = `${version}:${path}`
|
||||
const $ = await this.getRenderedPageObj(pathCacheKey, context)
|
||||
|
||||
const imageSrcs = $('img[src^="/assets"]').map((i, el) => $(el).attr('src')).toArray()
|
||||
|
||||
this.addImagesForLater(imageSrcs, pathCacheKey)
|
||||
|
||||
for (const href of this.internalHrefPrefixes) {
|
||||
const internalLinks = $(`a[href^="${href}"]`).get()
|
||||
|
||||
for (const internalLink of internalLinks) {
|
||||
const href = $(internalLink).attr('href')
|
||||
|
||||
let [link, anchor] = href.split('#')
|
||||
// remove trailing slash
|
||||
link = link.replace(patterns.trailingSlash, '$1')
|
||||
|
||||
// if it's an external link and has been checked before, skip
|
||||
if (link && this.checkedLinksCache.has(link)) {
|
||||
// if it's been determined this link is broken, add to the linkedFrom field
|
||||
if (this.brokenLinks.has(link)) {
|
||||
const brokenLink = this.brokenLinks.get(link)
|
||||
brokenLink.linkedFrom = union(brokenLink.linkedFrom, [pathCacheKey])
|
||||
this.brokenLinks.set(link, brokenLink)
|
||||
}
|
||||
if (!anchor) continue
|
||||
}
|
||||
|
||||
// if it's an internal anchor (e.g., #foo), save for later
|
||||
if (anchor && !link) {
|
||||
// ignore anchors that are autogenerated from headings
|
||||
if (anchor === $(internalLink).parent().attr('id')) continue
|
||||
this.addAnchorForLater(pathCacheKey, anchor, 'same page')
|
||||
continue
|
||||
}
|
||||
|
||||
// ------ BEGIN ONEOFF EXCLUSIONS -------///
|
||||
// skip GraphQL public schema paths (these are checked by separate tests)
|
||||
if (link.startsWith('/public/') && link.endsWith('.graphql')) continue
|
||||
|
||||
// skip links that start with /assets/images, as these are not in the pages collection
|
||||
// and /assets/images paths should be checked during the image check
|
||||
if (link.startsWith('/assets/images')) continue
|
||||
|
||||
// skip rare hardcoded links to old GHE versions
|
||||
// these paths will always be in the old versioned format
|
||||
// example: /enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release
|
||||
const gheVersionInLink = link.match(patterns.getEnterpriseVersionNumber)
|
||||
if (gheVersionInLink && deprecated.includes(gheVersionInLink[1])) continue
|
||||
// ------ END ONEOFF EXCLUSIONS -------///
|
||||
|
||||
// the link at this point should include a version via lib/rewrite-local-links
|
||||
const versionFromHref = getVersionStringFromPath(link)
|
||||
|
||||
// look for linked page
|
||||
const linkedPage = findPageInVersion(link, context.pages, context.redirects, this.languageCode, versionFromHref)
|
||||
this.checkedLinksCache.add(link)
|
||||
|
||||
if (!linkedPage) {
|
||||
this.brokenLinks.set(link, { linkedFrom: [pathCacheKey] })
|
||||
continue
|
||||
}
|
||||
|
||||
// if we're not checking external anchors, we're done
|
||||
if (!checkExternalAnchors) {
|
||||
continue
|
||||
}
|
||||
|
||||
// find the permalink for the current version
|
||||
const linkedPagePermalink = linkedPage.permalinks.find(permalink => permalink.pageVersion === version)
|
||||
|
||||
if (linkedPagePermalink) {
|
||||
const linkedPageContext = await buildPathContext(context, linkedPage, linkedPagePermalink)
|
||||
|
||||
if (anchor) {
|
||||
await this.setRenderedPageObj(`${version}:${linkedPage.relativePath}`, linkedPageContext)
|
||||
this.addAnchorForLater(`${version}:${linkedPage.relativePath}`, anchor, pathCacheKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkAnchors () {
|
||||
for await (const [pathCacheKey, anchors] of this.anchorLinksToCheck) {
|
||||
const $ = await this.getRenderedPageObj(pathCacheKey)
|
||||
for (const anchorText in anchors) {
|
||||
const matchingHeadings = $(`[id="${anchorText}"], [name="${anchorText}"]`)
|
||||
if (matchingHeadings.length === 0) {
|
||||
const brokenAnchorPath = this.brokenAnchors.get(pathCacheKey) || {}
|
||||
brokenAnchorPath[anchorText] = anchors[anchorText]
|
||||
this.brokenAnchors.set(pathCacheKey, brokenAnchorPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBrokenLinks () {
|
||||
return this.brokenLinks
|
||||
}
|
||||
|
||||
async getBrokenAnchors () {
|
||||
await this.checkAnchors()
|
||||
return this.brokenAnchors
|
||||
}
|
||||
|
||||
async getBrokenImages () {
|
||||
for await (const [imageSrc, imageUsage] of this.imagesToCheck) {
|
||||
try {
|
||||
await fs.promises.access(path.join(process.cwd(), imageSrc))
|
||||
} catch (e) {
|
||||
this.brokenImages.set(imageSrc, imageUsage)
|
||||
}
|
||||
}
|
||||
return this.brokenImages
|
||||
}
|
||||
}
|
||||
|
||||
// this function is async because the middleware functions are likely async
|
||||
async function applyMiddleware (middleware, req) {
|
||||
return middleware(req, null, () => {})
|
||||
}
|
||||
|
||||
async function buildInitialContext () {
|
||||
const req = {
|
||||
path: '/en',
|
||||
language: 'en',
|
||||
query: {}
|
||||
}
|
||||
await applyMiddleware(contextualize, req)
|
||||
return req.context
|
||||
}
|
||||
|
||||
async function buildPathContext (initialContext, page, permalink) {
|
||||
// Create a new object with path-specific properties.
|
||||
// Note this is cherry-picking properties currently only needed by the middlware below;
|
||||
// See middleware/context.js for the rest of the properties we are NOT refreshing per page.
|
||||
// If we find this causes problems for link checking, we can call `contextualize` on
|
||||
// every page. For now, this cherry-picking approach is intended to improve performance so
|
||||
// we don't have to build the expensive `pages`, `redirects`, etc. data on every page we check.
|
||||
const pathContext = {
|
||||
page,
|
||||
currentVersion: permalink.pageVersion,
|
||||
relativePath: permalink.relativePath,
|
||||
currentPath: permalink.href
|
||||
}
|
||||
|
||||
// Combine it with the initial context object that has pages, redirects, etc.
|
||||
const combinedContext = Object.assign({}, initialContext, pathContext)
|
||||
|
||||
// Create a new req object using the combined context
|
||||
const req = {
|
||||
path: permalink.href,
|
||||
context: combinedContext,
|
||||
language: 'en',
|
||||
query: {}
|
||||
}
|
||||
|
||||
// Pass the req to the contextualizing middlewares
|
||||
await applyMiddleware(rest, req)
|
||||
await applyMiddleware(graphql, req)
|
||||
// Release notes are available on docs site starting with GHES 3.0
|
||||
if (versionSatisfiesRange(permalink.pageVersion, '>=3.0')) {
|
||||
await applyMiddleware(releaseNotes, req)
|
||||
}
|
||||
|
||||
// Return the resulting context object with REST, GraphQL, and release notes data now attached
|
||||
return req.context
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LinksChecker,
|
||||
buildPathContext,
|
||||
buildInitialContext
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
const flat = require('flat')
|
||||
const { last } = require('lodash')
|
||||
const cheerio = require('cheerio')
|
||||
const { loadPages, loadPageMap } = require('../../lib/pages')
|
||||
const loadSiteData = require('../../lib/site-data')
|
||||
const getApplicableVersions = require('../../lib/get-applicable-versions')
|
||||
const loadRedirects = require('../../lib/redirects/precompile')
|
||||
const { getVersionedPathWithLanguage } = require('../../lib/path-utils')
|
||||
const renderContent = require('../../lib/render-content')
|
||||
const checkImages = require('../../lib/check-images')
|
||||
const checkLinks = require('../../lib/check-developer-links')
|
||||
const allVersions = require('../../lib/all-versions')
|
||||
const enterpriseServerVersions = Object.keys(require('../../lib/all-versions'))
|
||||
.filter(version => version.startsWith('enterprise-server@'))
|
||||
|
||||
// schema-derived data to add to context object
|
||||
const rest = require('../../lib/rest')
|
||||
const previews = require('../../lib/graphql/static/previews')
|
||||
const upcomingChanges = require('../../lib/graphql/static/upcoming-changes')
|
||||
const changelog = require('../../lib/graphql/static/changelog')
|
||||
const prerenderedObjects = require('../../lib/graphql/static/prerendered-objects')
|
||||
|
||||
// english only
|
||||
const languageCode = 'en'
|
||||
|
||||
const context = {
|
||||
currentLanguage: languageCode,
|
||||
rest
|
||||
}
|
||||
|
||||
// developer content only
|
||||
const developerContentRegex = /^(rest|graphql|developers)/
|
||||
|
||||
describe('page rendering', () => {
|
||||
jest.setTimeout(1000 * 1000)
|
||||
|
||||
const brokenImages = {}
|
||||
const brokenAnchors = {}
|
||||
const brokenLinks = {}
|
||||
|
||||
beforeAll(async (done) => {
|
||||
const pageList = await loadPages()
|
||||
const pageMap = await loadPageMap(pageList)
|
||||
const siteData = await loadSiteData()
|
||||
const redirects = await loadRedirects(pageList, pageMap)
|
||||
|
||||
context.pages = pageMap
|
||||
context.site = siteData[languageCode].site
|
||||
context.redirects = redirects
|
||||
|
||||
const developerPages = pageList
|
||||
.filter(page => page.relativePath.match(developerContentRegex) && page.languageCode === languageCode)
|
||||
|
||||
let checkedLinks = {}
|
||||
let checkedImages = {}
|
||||
|
||||
for (const page of developerPages) {
|
||||
const brokenImagesPerPage = {}
|
||||
const brokenAnchorsPerPage = {}
|
||||
const brokenLinksPerPage = {}
|
||||
|
||||
// get an array of the pages product versions
|
||||
const pageVersions = getApplicableVersions(page.versions, page.relativePath)
|
||||
|
||||
for (const pageVersion of pageVersions) {
|
||||
// attach page-specific properties to context
|
||||
page.version = pageVersion
|
||||
context.page = page
|
||||
context.currentVersion = pageVersion
|
||||
context.enterpriseServerVersions = enterpriseServerVersions
|
||||
|
||||
const relevantPermalink = page.permalinks.find(permalink => permalink.pageVersion === pageVersion)
|
||||
|
||||
const graphqlVersion = allVersions[pageVersion].miscVersionName
|
||||
|
||||
// borrowed from middleware/contextualizers/graphql.js
|
||||
context.graphql = {
|
||||
schemaForCurrentVersion: require(`../../lib/graphql/static/schema-${graphqlVersion}`),
|
||||
previewsForCurrentVersion: previews[graphqlVersion],
|
||||
upcomingChangesForCurrentVersion: upcomingChanges[graphqlVersion],
|
||||
prerenderedObjectsForCurrentVersion: prerenderedObjects[graphqlVersion],
|
||||
changelog
|
||||
}
|
||||
|
||||
// borrowed from middleware/contextualizers/rest.js
|
||||
context.restGitHubAppsLink = getVersionedPathWithLanguage(
|
||||
'/developers/apps',
|
||||
pageVersion,
|
||||
languageCode
|
||||
)
|
||||
|
||||
context.operationsForCurrentProduct = context.rest.operations[pageVersion] || []
|
||||
|
||||
if (relevantPermalink.href.includes('rest/reference/')) {
|
||||
const docsPath = relevantPermalink.href
|
||||
.split('rest/reference/')[1]
|
||||
.split('#')[0] // do not include #fragments
|
||||
|
||||
// find all operations that with an operationID that matches the requested docs path
|
||||
context.currentRestOperations = context.operationsForCurrentProduct
|
||||
.filter(operation => operation.operationId.startsWith(docsPath))
|
||||
}
|
||||
|
||||
// collect elements of the page that may contain links
|
||||
const pageContent = relevantPermalink.href.includes('graphql/reference/objects')
|
||||
? page.markdown + context.graphql.prerenderedObjectsForCurrentVersion.html
|
||||
: page.intro + page.permissions + page.markdown
|
||||
|
||||
// renderContent is much faster than page.render, even though we later have to run
|
||||
// rewriteLocalLinks in check-images and rewriteAssetPathsToS3 in check-links
|
||||
const pageHtml = await renderContent(pageContent, context)
|
||||
const $ = cheerio.load(pageHtml, { xmlMode: true })
|
||||
|
||||
// check images
|
||||
const { brokenImages: brokenImagesPerVersion, checkedImageCache } = await checkImages($, pageVersion, page.relativePath, checkedImages)
|
||||
if (brokenImagesPerVersion.length) brokenImagesPerPage[pageVersion] = brokenImagesPerVersion
|
||||
checkedImages = checkedImageCache
|
||||
|
||||
// check anchors and links
|
||||
const { brokenLinks: brokenLinksPerVersion, checkedLinkCache } = await checkLinks($, page, context, pageVersion, checkedLinks)
|
||||
if (brokenLinksPerVersion.anchors.length) brokenAnchorsPerPage[pageVersion] = brokenLinksPerVersion.anchors
|
||||
if (brokenLinksPerVersion.links.length) brokenLinksPerPage[pageVersion] = brokenLinksPerVersion.links
|
||||
checkedLinks = checkedLinkCache
|
||||
}
|
||||
|
||||
if (Object.keys(brokenImagesPerPage).length) brokenImages[page.fullPath] = brokenImagesPerPage
|
||||
if (Object.keys(brokenAnchorsPerPage).length) brokenAnchors[page.fullPath] = brokenAnchorsPerPage
|
||||
if (Object.keys(brokenLinksPerPage).length) brokenLinks[page.fullPath] = brokenLinksPerPage
|
||||
}
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
test('every page has image references that can be resolved', async () => {
|
||||
const numbrokenImages = getNumBrokenItems(brokenImages)
|
||||
expect(numbrokenImages, `Found ${numbrokenImages} total broken images: ${JSON.stringify(brokenImages, null, 2)}`).toBe(0)
|
||||
})
|
||||
|
||||
test.skip('every page has links with anchors that can be resolved', async () => {
|
||||
const numbrokenAnchors = getNumBrokenItems(brokenAnchors)
|
||||
expect(numbrokenAnchors, `Found ${numbrokenAnchors} total broken anchors: ${JSON.stringify(brokenAnchors, null, 2)}`).toBe(0)
|
||||
})
|
||||
|
||||
// disable anchor test til we resolve broken anchors
|
||||
test.skip('every page has links that can be resolved', async () => {
|
||||
const numbrokenLinks = getNumBrokenItems(brokenLinks)
|
||||
expect(numbrokenLinks, `Found ${numbrokenLinks} total broken links: ${JSON.stringify(brokenLinks, null, 2)}`).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
// count all the nested items
|
||||
function getNumBrokenItems (items) {
|
||||
// filter for entries like this:
|
||||
// '/article-path-here.md.dotcom.1.broken link': '/en/articles/foo',
|
||||
return Object.keys(flat(items))
|
||||
.filter(key => last(key.split('.')).includes('broken'))
|
||||
.length
|
||||
}
|
||||
@@ -1,113 +1,47 @@
|
||||
const cheerio = require('cheerio')
|
||||
const { loadPages, loadPageMap } = require('../../lib/pages')
|
||||
const loadSiteData = require('../../lib/site-data')
|
||||
const getApplicableVersions = require('../../lib/get-applicable-versions')
|
||||
const renderContent = require('../../lib/render-content')
|
||||
const checkImages = require('../../lib/check-images')
|
||||
const checkLinks = require('../../lib/check-links')
|
||||
const enterpriseServerVersions = Object.keys(require('../../lib/all-versions'))
|
||||
.filter(version => version.startsWith('enterprise-server@'))
|
||||
const flat = require('flat')
|
||||
const { last } = require('lodash')
|
||||
|
||||
// english only for now
|
||||
const { LinksChecker, buildInitialContext, buildPathContext } = require('../helpers/links-checker')
|
||||
const { uniq } = require('lodash')
|
||||
const languageCode = 'en'
|
||||
|
||||
const context = { currentLanguage: languageCode }
|
||||
|
||||
const loadRedirects = require('../../lib/redirects/precompile')
|
||||
// TODO set to true when we're ready to report and fix broken anchors
|
||||
const checkExternalAnchors = false
|
||||
|
||||
describe('page rendering', () => {
|
||||
jest.setTimeout(1000 * 1000)
|
||||
|
||||
const brokenImages = {}
|
||||
const brokenAnchors = {}
|
||||
const brokenLinks = {}
|
||||
const linksChecker = new LinksChecker()
|
||||
|
||||
beforeAll(async (done) => {
|
||||
const pageList = await loadPages()
|
||||
const pageMap = await loadPageMap(pageList)
|
||||
const siteData = await loadSiteData()
|
||||
const redirects = await loadRedirects(pageList, pageMap)
|
||||
// fetch context.pages, context.redirects, etc.
|
||||
// we only want to build these one time
|
||||
const context = await buildInitialContext()
|
||||
|
||||
context.pages = pageMap
|
||||
context.site = siteData[languageCode].site
|
||||
context.redirects = redirects
|
||||
|
||||
let checkedLinks = {}
|
||||
let checkedImages = {}
|
||||
|
||||
const englishPages = pageList
|
||||
const englishPages = uniq(Object.values(context.pages))
|
||||
.filter(page => page.languageCode === languageCode)
|
||||
// ignore developers content, to be checked separately
|
||||
.filter(page => !page.relativePath.match(/^(rest|graphql|developers)/))
|
||||
|
||||
for (const page of englishPages) {
|
||||
// skip map topics because they have no content of their own
|
||||
if (page.mapTopic) continue
|
||||
|
||||
const brokenImagesPerPage = {}
|
||||
const brokenAnchorsPerPage = {}
|
||||
const brokenLinksPerPage = {}
|
||||
|
||||
// get an array of the pages product versions
|
||||
const pageVersions = getApplicableVersions(page.versions, page.relativePath)
|
||||
|
||||
for (const pageVersion of pageVersions) {
|
||||
// attach page-specific properties to context
|
||||
page.version = pageVersion
|
||||
context.page = page
|
||||
context.currentVersion = pageVersion
|
||||
context.enterpriseServerVersions = enterpriseServerVersions
|
||||
|
||||
// collect elements of the page that may contain links
|
||||
const pageContent = page.intro + page.permissions + page.markdown
|
||||
|
||||
// renderContent is much faster than page.render, even though we later have to run
|
||||
// rewriteLocalLinks in check-images and rewriteAssetPathsToS3 in check-links
|
||||
const pageHtml = await renderContent(pageContent, context)
|
||||
const $ = cheerio.load(pageHtml, { xmlMode: true })
|
||||
|
||||
// check images
|
||||
const { brokenImages: brokenImagesPerVersion, checkedImageCache } = await checkImages($, pageVersion, page.relativePath, checkedImages)
|
||||
if (brokenImagesPerVersion.length) brokenImagesPerPage[pageVersion] = brokenImagesPerVersion
|
||||
checkedImages = checkedImageCache
|
||||
|
||||
// check anchors and links
|
||||
const { brokenLinks: brokenLinksPerVersion, checkedLinkCache } = await checkLinks($, page, context, pageVersion, checkedLinks)
|
||||
if (brokenLinksPerVersion.anchors.length) brokenAnchorsPerPage[pageVersion] = brokenLinksPerVersion.anchors
|
||||
if (brokenLinksPerVersion.links.length) brokenLinksPerPage[pageVersion] = brokenLinksPerVersion.links
|
||||
checkedLinks = checkedLinkCache
|
||||
for (const permalink of page.permalinks) {
|
||||
const pathContext = await buildPathContext(context, page, permalink)
|
||||
await linksChecker.checkPage(pathContext, checkExternalAnchors)
|
||||
}
|
||||
|
||||
if (Object.keys(brokenImagesPerPage).length) brokenImages[page.fullPath] = brokenImagesPerPage
|
||||
if (Object.keys(brokenAnchorsPerPage).length) brokenAnchors[page.fullPath] = brokenAnchorsPerPage
|
||||
if (Object.keys(brokenLinksPerPage).length) brokenLinks[page.fullPath] = brokenLinksPerPage
|
||||
}
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
test('every page has image references that can be resolved', async () => {
|
||||
const numbrokenImages = getNumBrokenItems(brokenImages)
|
||||
expect(numbrokenImages, `Found ${numbrokenImages} total broken images: ${JSON.stringify(brokenImages, null, 2)}`).toBe(0)
|
||||
const result = await linksChecker.getBrokenImages()
|
||||
expect(result.size, `Found ${result.size} total broken images: ${JSON.stringify([...result], null, 2)}`).toBe(0)
|
||||
})
|
||||
|
||||
test('every page has links with anchors that can be resolved', async () => {
|
||||
const numbrokenAnchors = getNumBrokenItems(brokenAnchors)
|
||||
expect(numbrokenAnchors, `Found ${numbrokenAnchors} total broken anchors: ${JSON.stringify(brokenAnchors, null, 2)}`).toBe(0)
|
||||
// When ready to unskip this,
|
||||
test.skip('every page has links with anchors that can be resolved', async () => {
|
||||
const result = await linksChecker.getBrokenAnchors()
|
||||
const numBrokenAnchors = [...result].reduce((accumulator, [path, anchors]) => accumulator + Object.keys(anchors).length, 0)
|
||||
expect(numBrokenAnchors, `Found ${numBrokenAnchors} total broken anchors in ${result.size} pages: ${JSON.stringify([...result], null, 2)}`).toBe(0)
|
||||
})
|
||||
|
||||
test('every page has links that can be resolved', async () => {
|
||||
const numbrokenLinks = getNumBrokenItems(brokenLinks)
|
||||
expect(numbrokenLinks, `Found ${numbrokenLinks} total broken links: ${JSON.stringify(brokenLinks, null, 2)}`).toBe(0)
|
||||
test('every page has links that can be resolved', () => {
|
||||
const result = linksChecker.getBrokenLinks()
|
||||
expect(result.size, `Found ${result.size} total broken links: ${JSON.stringify([...result], null, 2)}`).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
// count all the nested items
|
||||
function getNumBrokenItems (items) {
|
||||
// filter for entries like this:
|
||||
// '/article-path-here.md.dotcom.1.broken link': '/en/articles/foo',
|
||||
return Object.keys(flat(items))
|
||||
.filter(key => last(key.split('.')).includes('broken'))
|
||||
.length
|
||||
}
|
||||
|
||||
117
tests/rendering/block-robots.js
Normal file
117
tests/rendering/block-robots.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const { blockIndex } = require('../../middleware/block-robots')
|
||||
const languages = require('../../lib/languages')
|
||||
const products = require('../../lib/all-products')
|
||||
const enterpriseServerReleases = require('../../lib/enterprise-server-releases')
|
||||
|
||||
function allowIndex (path) {
|
||||
return !blockIndex(path)
|
||||
}
|
||||
|
||||
describe('block robots', () => {
|
||||
it('allows crawling of the homepage and English content', async () => {
|
||||
expect(allowIndex('/')).toBe(true)
|
||||
expect(allowIndex('/en')).toBe(true)
|
||||
expect(allowIndex('/en/articles/verifying-your-email-address')).toBe(true)
|
||||
})
|
||||
|
||||
it('allows crawling of generally available localized content', async () => {
|
||||
Object.values(languages)
|
||||
.filter(language => !language.wip)
|
||||
.forEach(language => {
|
||||
expect(allowIndex(`/${language.code}`)).toBe(true)
|
||||
expect(allowIndex(`/${language.code}/articles/verifying-your-email-address`)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('disallows crawling of WIP localized content', async () => {
|
||||
Object.values(languages)
|
||||
.filter(language => language.wip)
|
||||
.forEach(language => {
|
||||
expect(allowIndex(`/${language.code}`)).toBe(false)
|
||||
expect(allowIndex(`/${language.code}/articles/verifying-your-email-address`)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('disallows crawling of WIP products', async () => {
|
||||
const wipProductIds = Object.values(products)
|
||||
.filter(product => product.wip)
|
||||
.map(product => product.id)
|
||||
|
||||
wipProductIds.forEach(id => {
|
||||
const { href } = products[id]
|
||||
const blockedPaths = [
|
||||
// English
|
||||
`/en${href}`,
|
||||
`/en${href}/overview`,
|
||||
`/en${href}/overview/intro`,
|
||||
`/en/enterprise/${enterpriseServerReleases.latest}/user${href}`,
|
||||
`/en/enterprise/${enterpriseServerReleases.oldestSupported}/user${href}`,
|
||||
|
||||
// Japanese
|
||||
`/ja${href}`,
|
||||
`/ja${href}/overview`,
|
||||
`/ja${href}/overview/intro`,
|
||||
`/ja/enterprise/${enterpriseServerReleases.latest}/user${href}`,
|
||||
`/ja/enterprise/${enterpriseServerReleases.oldestSupported}/user${href}`
|
||||
]
|
||||
|
||||
blockedPaths.forEach(path => {
|
||||
expect(allowIndex(path)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('disallows crawling of early access "hidden" products', async () => {
|
||||
const hiddenProductIds = Object.values(products)
|
||||
.filter(product => product.hidden)
|
||||
.map(product => product.id)
|
||||
|
||||
hiddenProductIds.forEach(id => {
|
||||
const { versions } = products[id]
|
||||
const blockedPaths = versions.map(version => {
|
||||
return [
|
||||
// English
|
||||
`/en/${version}/${id}`,
|
||||
`/en/${version}/${id}/some-early-access-article`,
|
||||
// Japanese
|
||||
`/ja/${version}/${id}`,
|
||||
`/ja/${version}/${id}/some-early-access-article`
|
||||
]
|
||||
}).flat()
|
||||
|
||||
blockedPaths.forEach(path => {
|
||||
expect(allowIndex(path)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('allows crawling of non-WIP products', async () => {
|
||||
expect('actions' in products).toBe(true)
|
||||
expect(allowIndex('/en/actions')).toBe(true)
|
||||
expect(allowIndex('/en/actions/overview')).toBe(true)
|
||||
expect(allowIndex('/en/actions/overview/intro')).toBe(true)
|
||||
expect(allowIndex(`/en/enterprise/${enterpriseServerReleases.latest}/user/actions`)).toBe(true)
|
||||
expect(allowIndex(`/en/enterprise/${enterpriseServerReleases.oldestSupported}/user/actions`)).toBe(true)
|
||||
})
|
||||
|
||||
it('disallows crawling of deprecated enterprise releases', async () => {
|
||||
enterpriseServerReleases.deprecated.forEach(version => {
|
||||
const blockedPaths = [
|
||||
// English
|
||||
`/en/enterprise-server@${version}/actions`,
|
||||
`/en/enterprise/${version}/actions`,
|
||||
`/en/enterprise-server@${version}/actions/overview`,
|
||||
`/en/enterprise/${version}/actions/overview`,
|
||||
// Japanese
|
||||
`/ja/enterprise-server@${version}/actions`,
|
||||
`/ja/enterprise/${version}/actions`,
|
||||
`/ja/enterprise-server@${version}/actions/overview`,
|
||||
`/ja/enterprise/${version}/actions/overview`
|
||||
]
|
||||
|
||||
blockedPaths.forEach(path => {
|
||||
expect(allowIndex(path)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -88,7 +88,7 @@ describe('breadcrumbs', () => {
|
||||
const breadcrumbs = await getJSON('/en/github?json=breadcrumbs')
|
||||
const expected = {
|
||||
product: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github`,
|
||||
title: 'GitHub.com'
|
||||
}
|
||||
}
|
||||
@@ -99,11 +99,11 @@ describe('breadcrumbs', () => {
|
||||
const breadcrumbs = await getJSON('/en/github/authenticating-to-github?json=breadcrumbs')
|
||||
const expected = {
|
||||
product: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github`,
|
||||
title: 'GitHub.com'
|
||||
},
|
||||
category: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github/authenticating-to-github`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github/authenticating-to-github`,
|
||||
title: 'Authentication'
|
||||
}
|
||||
}
|
||||
@@ -114,15 +114,15 @@ describe('breadcrumbs', () => {
|
||||
const breadcrumbs = await getJSON('/en/github/authenticating-to-github/keeping-your-account-and-data-secure?json=breadcrumbs')
|
||||
const expected = {
|
||||
product: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github`,
|
||||
title: 'GitHub.com'
|
||||
},
|
||||
category: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github/authenticating-to-github`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github/authenticating-to-github`,
|
||||
title: 'Authentication'
|
||||
},
|
||||
maptopic: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github/authenticating-to-github/keeping-your-account-and-data-secure`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github/authenticating-to-github/keeping-your-account-and-data-secure`,
|
||||
title: 'Keeping your account and data secure'
|
||||
}
|
||||
}
|
||||
@@ -133,19 +133,19 @@ describe('breadcrumbs', () => {
|
||||
const breadcrumbs = await getJSON('/en/github/authenticating-to-github/creating-a-strong-password?json=breadcrumbs')
|
||||
const expected = {
|
||||
product: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github`,
|
||||
title: 'GitHub.com'
|
||||
},
|
||||
category: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github/authenticating-to-github`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github/authenticating-to-github`,
|
||||
title: 'Authentication'
|
||||
},
|
||||
maptopic: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github/authenticating-to-github/keeping-your-account-and-data-secure`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github/authenticating-to-github/keeping-your-account-and-data-secure`,
|
||||
title: 'Keeping your account and data secure'
|
||||
},
|
||||
article: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github/authenticating-to-github/creating-a-strong-password`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github/authenticating-to-github/creating-a-strong-password`,
|
||||
title: 'Creating a strong password'
|
||||
}
|
||||
}
|
||||
@@ -156,15 +156,15 @@ describe('breadcrumbs', () => {
|
||||
const breadcrumbs = await getJSON('/github/site-policy/github-privacy-statement?json=breadcrumbs')
|
||||
const expected = {
|
||||
product: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github`,
|
||||
title: 'GitHub.com'
|
||||
},
|
||||
category: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github/site-policy`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github/site-policy`,
|
||||
title: 'Site policy'
|
||||
},
|
||||
article: {
|
||||
href: `/${nonEnterpriseDefaultVersion}/github/site-policy/github-privacy-statement`,
|
||||
href: `/en/${nonEnterpriseDefaultVersion}/github/site-policy/github-privacy-statement`,
|
||||
title: 'GitHub Privacy Statement'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ const robotsParser = require('robots-parser')
|
||||
const robotsMiddleware = require('../../middleware/robots')
|
||||
const { get } = require('../helpers/supertest')
|
||||
const MockExpressResponse = require('mock-express-response')
|
||||
const products = require('../../lib/all-products')
|
||||
const enterpriseServerReleases = require('../../lib/enterprise-server-releases')
|
||||
|
||||
describe('robots.txt', () => {
|
||||
jest.setTimeout(5 * 60 * 1000)
|
||||
@@ -31,15 +29,6 @@ describe('robots.txt', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('disallows indexing of WIP localized content', async () => {
|
||||
Object.values(languages)
|
||||
.filter(language => language.wip)
|
||||
.forEach(language => {
|
||||
expect(robots.isAllowed(`https://docs.github.com/${language.code}`)).toBe(false)
|
||||
expect(robots.isAllowed(`https://docs.github.com/${language.code}/articles/verifying-your-email-address`)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('disallows indexing of herokuapp.com domains', async () => {
|
||||
const req = {
|
||||
hostname: 'docs-internal-12345--my-branch.herokuapp.com',
|
||||
@@ -52,89 +41,6 @@ describe('robots.txt', () => {
|
||||
expect(res._getString()).toEqual('User-agent: *\nDisallow: /')
|
||||
})
|
||||
|
||||
it('disallows indexing of WIP products', async () => {
|
||||
const wipProductIds = Object.values(products)
|
||||
.filter(product => product.wip)
|
||||
.map(product => product.id)
|
||||
|
||||
wipProductIds.forEach(id => {
|
||||
const { href } = products[id]
|
||||
const blockedPaths = [
|
||||
// English
|
||||
`https://docs.github.com/en${href}`,
|
||||
`https://docs.github.com/en${href}/overview`,
|
||||
`https://docs.github.com/en${href}/overview/intro`,
|
||||
`https://docs.github.com/en/enterprise/${enterpriseServerReleases.latest}/user${href}`,
|
||||
`https://docs.github.com/en/enterprise/${enterpriseServerReleases.oldestSupported}/user${href}`,
|
||||
|
||||
// Japanese
|
||||
`https://docs.github.com/ja${href}`,
|
||||
`https://docs.github.com/ja${href}/overview`,
|
||||
`https://docs.github.com/ja${href}/overview/intro`,
|
||||
`https://docs.github.com/ja/enterprise/${enterpriseServerReleases.latest}/user${href}`,
|
||||
`https://docs.github.com/ja/enterprise/${enterpriseServerReleases.oldestSupported}/user${href}`
|
||||
]
|
||||
|
||||
blockedPaths.forEach(path => {
|
||||
expect(robots.isAllowed(path)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('disallows indexing of early access "hidden" products', async () => {
|
||||
const hiddenProductIds = Object.values(products)
|
||||
.filter(product => product.hidden)
|
||||
.map(product => product.id)
|
||||
|
||||
hiddenProductIds.forEach(id => {
|
||||
const { versions } = products[id]
|
||||
const blockedPaths = versions.map(version => {
|
||||
return [
|
||||
// English
|
||||
`https://docs.github.com/en/${version}/${id}`,
|
||||
`https://docs.github.com/en/${version}/${id}/some-early-access-article`,
|
||||
// Japanese
|
||||
`https://docs.github.com/ja/${version}/${id}`,
|
||||
`https://docs.github.com/ja/${version}/${id}/some-early-access-article`
|
||||
]
|
||||
}).flat()
|
||||
|
||||
blockedPaths.forEach(path => {
|
||||
expect(robots.isAllowed(path)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('allows indexing of non-WIP products', async () => {
|
||||
expect('actions' in products).toBe(true)
|
||||
expect(robots.isAllowed('https://docs.github.com/en/actions')).toBe(true)
|
||||
expect(robots.isAllowed('https://docs.github.com/en/actions/overview')).toBe(true)
|
||||
expect(robots.isAllowed('https://docs.github.com/en/actions/overview/intro')).toBe(true)
|
||||
expect(robots.isAllowed(`https://docs.github.com/en/enterprise/${enterpriseServerReleases.latest}/user/actions`)).toBe(true)
|
||||
expect(robots.isAllowed(`https://docs.github.com/en/enterprise/${enterpriseServerReleases.oldestSupported}/user/actions`)).toBe(true)
|
||||
})
|
||||
|
||||
it('disallows indexing of deprecated enterprise releases', async () => {
|
||||
enterpriseServerReleases.deprecated.forEach(version => {
|
||||
const blockedPaths = [
|
||||
// English
|
||||
`https://docs.github.com/en/enterprise-server@${version}/actions`,
|
||||
`https://docs.github.com/en/enterprise/${version}/actions`,
|
||||
`https://docs.github.com/en/enterprise-server@${version}/actions/overview`,
|
||||
`https://docs.github.com/en/enterprise/${version}/actions/overview`,
|
||||
// Japanese
|
||||
`https://docs.github.com/ja/enterprise-server@${version}/actions`,
|
||||
`https://docs.github.com/ja/enterprise/${version}/actions`,
|
||||
`https://docs.github.com/ja/enterprise-server@${version}/actions/overview`,
|
||||
`https://docs.github.com/ja/enterprise/${version}/actions/overview`
|
||||
]
|
||||
|
||||
blockedPaths.forEach(path => {
|
||||
expect(robots.isAllowed(path)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('does not have duplicate lines', () => {
|
||||
const lines = new Set()
|
||||
for (const line of res.text.split('\n')) {
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('enterprise deprecation', () => {
|
||||
test('sets the expected x-robots-tag header for deprecated Enterprise pages', async () => {
|
||||
const res = await get('/en/enterprise/2.13/user/articles/about-branches')
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.get('x-robots-tag')).toBe('none')
|
||||
expect(res.get('x-robots-tag')).toBe('noindex')
|
||||
})
|
||||
|
||||
test('handles requests for deprecated Enterprise pages ( <2.13 )', async () => {
|
||||
|
||||
Reference in New Issue
Block a user