Merge branch 'main' into move-explorer-to-docs

This commit is contained in:
Sarah Schneider 2020-12-10 11:29:37 -05:00 коммит произвёл GitHub
Родитель bdc9f697ee d04341d7b7
Коммит d43acb967a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
103 изменённых файлов: 1327 добавлений и 724 удалений

2
.github/allowed-actions.js поставляемый
Просмотреть файл

@ -30,7 +30,7 @@ module.exports = [
'rachmari/labeler@832d42ec5523f3c6d46e8168de71cd54363e3e2e',
'repo-sync/github-sync@3832fe8e2be32372e1b3970bbae8e7079edeec88',
'repo-sync/pull-request@33777245b1aace1a58c87a29c90321aa7a74bd7d',
'rtCamp/action-slack-notify@e17352feaf9aee300bf0ebc1dfbf467d80438815',
'someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd',
'tjenkinson/gh-action-auto-merge-dependency-updates@cee2ac0',
'EndBug/add-and-commit@9358097a71ad9fb9e2f9624c6098c89193d83575'
]

72
.github/workflows/confirm-internal-staff-work-in-docs.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,72 @@
name: Confirm internal staff meant to post in public
on:
issues:
types:
- opened
- reopened
- transferred
pull_request_target:
types:
- opened
- reopened
jobs:
check-team-membership:
runs-on: ubuntu-latest
continue-on-error: true
if: github.repository == 'github/docs'
steps:
- uses: actions/github-script@626af12fe9a53dc2972b48385e7fe7dec79145c9
with:
github-token: ${{ secrets.DOCUBOT_FR_PROJECT_BOARD_WORKFLOWS_REPO_ORG_READ_SCOPES }}
script: |
// Only perform this action with GitHub employees
try {
await github.teams.getMembershipForUserInOrg({
org: 'github',
team_slug: 'employees',
username: context.payload.sender.login,
});
} catch(err) {
// An error will be thrown if the user is not a GitHub employee
// If a user is not a GitHub employee, we should stop here and
// Not send a notification
return
}
// Don't perform this action with Docs team members
try {
await github.teams.getMembershipForUserInOrg({
org: 'github',
team_slug: 'docs',
username: context.payload.sender.login,
});
// If the user is a Docs team member, we should stop here and not send
// a notification
return
} catch(err) {
// An error will be thrown if the user is not a Docs team member
// If a user is not a Docs team member we should continue and send
// the notification
}
const issueNo = context.number || context.issue.number
// Create an issue in our private repo
await github.issues.create({
owner: 'github',
repo: 'docs-internal',
title: `@${context.payload.sender.login} confirm that \#${issueNo} should be in the public github/docs repo`,
body: `@${context.payload.sender.login} opened https://github.com/github/docs/issues/${issueNo} publicly in the github/docs repo, instead of the private github/docs-internal repo.\n\n@${context.payload.sender.login}, please confirm that this belongs in the public repo and that no sensitive information was disclosed by commenting below and closing the issue.\n\nIf this was not intentional and sensitive information was shared, please delete https://github.com/github/docs/issues/${issueNo} and notify us in the \#docs-open-source channel.\n\nThanks! \n\n/cc @github/docs @github/docs-engineering`
});
throw new Error('A Hubber opened an issue on the public github/docs repo');
- name: Send Slack notification if a GitHub employee who isn't on the docs team opens an issue in public
if: ${{ failure() && github.repository == 'github/docs' }}
uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd
with:
channel: ${{ secrets.DOCS_OPEN_SOURCE_SLACK_CHANNEL_ID }}
bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
text: <@${{github.actor}}> opened https://github.com/github/docs/issues/${{ github.event.number || github.event.issue.number }} publicly on the github/docs repo instead of the private github/docs-internal repo. They have been notified via a new issue in the github/docs-internal repo to confirm this was intentional.

15
.github/workflows/js-lint.yml поставляемый
Просмотреть файл

@ -10,23 +10,8 @@ on:
- translations
jobs:
see_if_should_skip:
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@36feb0d8d062137530c2e00bd278d138fe191289
with:
cancel_others: 'false'
github_token: ${{ github.token }}
paths: '["**/*.js", "package*.json", ".github/workflows/js-lint.yml", ".eslint*"]'
lint:
runs-on: ubuntu-latest
needs: see_if_should_skip
if: ${{ needs.see_if_should_skip.outputs.should_skip != 'true' }}
steps:
- name: Check out repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f

13
.github/workflows/repo-freeze-reminders.yml поставляемый
Просмотреть файл

@ -14,11 +14,10 @@ jobs:
if: github.repository == 'github/docs-internal'
steps:
- name: Send Slack notification if repo is frozen
uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd
if: ${{ env.FREEZE == 'true' }}
uses: rtCamp/action-slack-notify@e17352feaf9aee300bf0ebc1dfbf467d80438815
env:
SLACK_WEBHOOK: ${{ secrets.DOCS_ALERTS_SLACK_WEBHOOK }}
SLACK_USERNAME: docs-repo-sync
SLACK_ICON_EMOJI: ':freezing_face:'
SLACK_COLOR: '#51A0D5' # Carolina Blue
SLACK_MESSAGE: All repo-sync runs will fail for ${{ github.repository }} because the repo is currently frozen!
with:
channel: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }}
bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
color: info
text: All repo-sync runs will fail for ${{ github.repository }} because the repo is currently frozen!

54
.github/workflows/repo-sync-stalls.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,54 @@
name: Repo Sync Stalls
on:
workflow_dispatch:
schedule:
- cron: '0 */2 * * *'
jobs:
check-freezer:
name: Check for deployment freezes
runs-on: ubuntu-latest
steps:
- name: Exit if repo is frozen
if: ${{ env.FREEZE == 'true' }}
run: |
echo 'The repo is currently frozen! Exiting this workflow.'
exit 1 # prevents further steps from running
repo-sync-stalls:
runs-on: ubuntu-latest
steps:
- name: Check if repo sync is stalled
uses: actions/github-script@626af12fe9a53dc2972b48385e7fe7dec79145c9
with:
github-token: ${{ secrets.DOCUBOT_FR_PROJECT_BOARD_WORKFLOWS_REPO_ORG_READ_SCOPES }}
script: |
let pulls;
const owner = context.repo.owner
const repo = context.repo.repo
try {
pulls = await github.pulls.list({
owner: owner,
repo: repo,
head: `${owner}:repo-sync`,
state: 'open'
});
} catch(err) {
throw err
return
}
pulls.data.forEach(pr => {
const timeDelta = Date.now() - Date.parse(pr.created_at);
const minutesOpen = timeDelta / 1000 / 60;
if (minutesOpen > 30) {
core.setFailed('Repo sync appears to be stalled')
}
})
- name: Send Slack notification if workflow fails
uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd
if: failure()
with:
channel: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }}
bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
color: failure
text: Repo sync appears to be stalled for ${{github.repository}}. See https://github.com/${{github.repository}}/pulls?q=is%3Apr+is%3Aopen+repo+sync

16
.github/workflows/repo-sync.yml поставляемый
Просмотреть файл

@ -7,6 +7,7 @@
name: Repo Sync
on:
workflow_dispatch:
schedule:
- cron: '*/15 * * * *' # every 15 minutes
@ -70,11 +71,10 @@ jobs:
number: ${{ steps.find-pull-request.outputs.number }}
- name: Send Slack notification if workflow fails
uses: rtCamp/action-slack-notify@e17352feaf9aee300bf0ebc1dfbf467d80438815
if: ${{ failure() }}
env:
SLACK_WEBHOOK: ${{ secrets.DOCS_ALERTS_SLACK_WEBHOOK }}
SLACK_USERNAME: docs-repo-sync
SLACK_ICON_EMOJI: ':ohno:'
SLACK_COLOR: '#B90E0A' # Crimson
SLACK_MESSAGE: The last repo-sync run for ${{github.repository}} failed. See https://github.com/${{github.repository}}/actions?query=workflow%3A%22Repo+Sync%22
uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd
if: failure()
with:
channel: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }}
bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
color: failure
text: The last repo-sync run for ${{github.repository}} failed. See https://github.com/${{github.repository}}/actions?query=workflow%3A%22Repo+Sync%22

Просмотреть файл

@ -33,8 +33,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run sync-search
- name: Send slack notification if workflow run fails
uses: rtCamp/action-slack-notify@e17352feaf9aee300bf0ebc1dfbf467d80438815
uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd
if: failure()
env:
SLACK_WEBHOOK: ${{ secrets.DOCS_ALERTS_SLACK_WEBHOOK }}
SLACK_MESSAGE: The last Algolia workflow run for ${{github.repository}} failed. Search actions for `workflow:Algolia`
with:
channel: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }}
bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
color: failure
text: The last Algolia workflow run for ${{github.repository}} failed. Search actions for `workflow:Algolia`

15
.github/workflows/yml-lint.yml поставляемый
Просмотреть файл

@ -10,23 +10,8 @@ on:
- translations
jobs:
see_if_should_skip:
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@36feb0d8d062137530c2e00bd278d138fe191289
with:
cancel_others: 'false'
github_token: ${{ github.token }}
paths: '["**/*.yml", "**/*.yaml", "package*.json", ".github/workflows/yml-lint.yml"]'
lint:
runs-on: ubuntu-latest
needs: see_if_should_skip
if: ${{ needs.see_if_should_skip.outputs.should_skip != 'true' }}
steps:
- name: Check out repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f

Просмотреть файл

@ -50,6 +50,8 @@ There are a few more things to know when you're getting started with this repo:
In addition to the README you're reading right now, this repo includes other READMEs that describe the purpose of each subdirectory in more detail:
- [content/README.md](content/README.md)
- [content/graphql/README.md](content/graphql/README.md)
- [content/rest/README.md](content/rest/README.md)
- [contributing/README.md](contributing/README.md)
- [data/README.md](data/README.md)
- [data/reusables/README.md](data/reusables/README.md)

Двоичные данные
assets/images/help/discussions/public-repo-settings.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 23 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 68 KiB

После

Ширина:  |  Высота:  |  Размер: 111 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 54 KiB

Просмотреть файл

@ -54,6 +54,8 @@ This means that a compromise of a single action within a workflow can be very si
**Warning:** The short version of the commit SHA is insecure and should never be used for specifying an action's Git reference. Because of how repository networks work, any user can fork the repository and push a crafted commit to it that collides with the short SHA. This causes subsequent clones at that SHA to fail because it becomes an ambiguous commit. As a result, any workflows that use the shortened SHA will immediately fail.
{% endwarning %}
* **Audit the source code of the action**
Ensure that the action is handling the content of your repository and secrets as expected. For example, check that secrets are not sent to unintended hosts, or are not inadvertently logged.
@ -92,10 +94,14 @@ This list describes the recommended approaches for accessing repository data wit
As a result, self-hosted runners should almost [never be used for public repositories](/actions/hosting-your-own-runners/about-self-hosted-runners#self-hosted-runner-security-with-public-repositories) on {% data variables.product.product_name %}, because any user can open pull requests against the repository and compromise the environment. Similarly, be cautious when using self-hosted runners on private repositories, as anyone who can fork the repository and open a PR (generally those with read-access to the repository) are able to compromise the self-hosted runner environment, including gaining access to secrets and the more privileged `GITHUB_TOKEN` which grants write-access permissions on the repository.
When a self-hosted runner is defined at the organization or enterprise level, {% data variables.product.product_name %} can schedule workflows from multiple repositories onto the same runner. Consequently, a security compromise of these environments can result in a wide impact. To help reduce the scope of a compromise, you can create boundaries by organizing your self-hosted runners into separate groups. For more information, see "[Managing access to self-hosted runners using groups](/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups)."
You should also consider the environment of the self-hosted runner machines:
- What sensitive information resides on the machine configured as a self-hosted runner? For example, private SSH keys, API access tokens, among others.
- Does the machine have network access to sensitive services? For example, Azure or AWS metadata services. The amount of sensitive information in this environment should be kept to a minimum, and you should always be mindful that any user capable of invoking workflows has access to this environment.
Some customers might attempt to partially mitigate these risks by implementing systems that automatically destroy the self-hosted runner after each job execution. However, this approach might not be as effective as intended, as there is no way to guarantee that a self-hosted runner only runs one job.
### Auditing {% data variables.product.prodname_actions %} events
You can use the audit log to monitor administrative tasks in an organization. The audit log records the type of action, when it was run, and which user account performed the action.
@ -132,5 +138,3 @@ The following tables describe the {% data variables.product.prodname_actions %}
| `action:org.runner_group_renamed` | Triggered when an organization admin renames a self-hosted runner group.
| `action:org.runner_group_runners_added` | Triggered when an organization admin [adds a self-hosted runner to a group](/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups#moving-a-self-hosted-runner-to-a-group).
| `action:org.runner_group_runners_removed` | Triggered when an organization admin removes a self-hosted runner from a group.

Просмотреть файл

@ -1,6 +1,6 @@
---
title: About GitHub Marketplace
intro: 'Learn the basics to prepare your app for review before joining {% data variables.product.prodname_marketplace %}.'
intro: 'Learn about {% data variables.product.prodname_marketplace %} where you can share your apps and actions publicly with all {% data variables.product.product_name %} users.'
redirect_from:
- /apps/marketplace/getting-started/
- /marketplace/getting-started
@ -14,52 +14,41 @@ versions:
{% data reusables.actions.actions-not-verified %}
To learn about publishing {% data variables.product.prodname_actions %} in the {% data variables.product.prodname_marketplace %}, see "[Publishing actions in GitHub Marketplace](/actions/creating-actions/publishing-actions-in-github-marketplace)."
To learn about publishing {% data variables.product.prodname_actions %} in {% data variables.product.prodname_marketplace %}, see "[Publishing actions in GitHub Marketplace](/actions/creating-actions/publishing-actions-in-github-marketplace)."
### Apps
You can list verified and unverified apps in {% data variables.product.prodname_marketplace %}. Unverified apps do not go through the security, testing, and verification cycle {% data variables.product.prodname_dotcom %} requires for verified apps.
Anyone can share their apps with other users on {% data variables.product.prodname_marketplace %} but only listings that are verified by {% data variables.product.company_short %} can include paid plans. For more information, see "[About verified creators](/developers/github-marketplace/about-verified-creators)."
Verified apps have a green badge in {% data variables.product.prodname_marketplace %}. Unverified apps have a grey badge next to their listing and are only available as free apps.
If you're interested in creating an app for {% data variables.product.prodname_marketplace %}, but you're new to {% data variables.product.prodname_github_apps %} or {% data variables.product.prodname_oauth_app %}s, see "[Building {% data variables.product.prodname_github_apps %}](/developers/apps/building-github-apps)" or "[Building {% data variables.product.prodname_oauth_app %}s](/developers/apps/building-oauth-apps)."
![Green verified and grey unverified badge](/assets/images/marketplace/marketplace_verified_badges.png)
If you're interested in creating an app for {% data variables.product.prodname_marketplace %}, but you're new to {% data variables.product.prodname_github_apps %} and {% data variables.product.prodname_oauth_app %}s, see "[Building apps](/apps/)."
{% data reusables.marketplace.github_apps_preferred %}, although you can list both OAuth and {% data variables.product.prodname_github_app %}s in {% data variables.product.prodname_marketplace %}. See "[Differences between GitHub and OAuth apps](/apps/differences-between-apps/)" for more details. To learn more about switching from OAuth to {% data variables.product.prodname_github_apps %}, see [Migrating OAuth Apps to {% data variables.product.prodname_github_app %}s](/apps/migrating-oauth-apps-to-github-apps/).
{% data reusables.marketplace.github_apps_preferred %}, although you can list both OAuth and {% data variables.product.prodname_github_app %}s in {% data variables.product.prodname_marketplace %}. For more information, see "[Differences between {% data variables.product.prodname_github_apps %} and {% data variables.product.prodname_oauth_app %}s](/apps/differences-between-apps/)" and "[Migrating {% data variables.product.prodname_oauth_app %}s to {% data variables.product.prodname_github_apps %}](/apps/migrating-oauth-apps-to-github-apps/)."
If you have questions about {% data variables.product.prodname_marketplace %}, please contact {% data variables.contact.contact_support %} directly.
#### Unverified Apps
### Publishing an app to {% data variables.product.prodname_marketplace %}
Unverified apps do not need to meet the "[Requirements for listing an app on {% data variables.product.prodname_marketplace %}](/marketplace/getting-started/requirements-for-listing-an-app-on-github-marketplace/)" or go through the "[Security review process](/marketplace/getting-started/security-review-process/)".
When you have finished creating your app, you can share it with other users by publishing it to {% data variables.product.prodname_marketplace %}. In summary, the process is:
{% data reusables.marketplace.unverified-apps %} Having a published paid plan will prevent you from being able to submit an unverified app. You must remove paid plans or keep them in draft mode before publishing an unverified app.
1. Review your app carefully to ensure that it will behave as expected in other repositories and that it follows best practice guidelines. For more information, see "[Security best practices for apps](/developers/github-marketplace/security-best-practices-for-apps)" and "[Requirements for listing an app](/developers/github-marketplace/requirements-for-listing-an-app#best-practice-for-customer-experience)."
To list your unverified app in {% data variables.product.prodname_marketplace %}, you only need to create a "[Listing on {% data variables.product.prodname_marketplace %}](/marketplace/listing-on-github-marketplace/)" and submit it as an unverified listing.
1. Add webhook events to the app to track user billing requests. For more information about the {% data variables.product.prodname_marketplace %} API, webhook events, and billing requests, see "[Using the {% data variables.product.prodname_marketplace %} API in your app](/developers/github-marketplace/using-the-github-marketplace-api-in-your-app)."
{% data reusables.marketplace.launch-with-free %}
1. Create a draft {% data variables.product.prodname_marketplace %} listing. For more information, see "[Drafting a listing for your app](/developers/github-marketplace/drafting-a-listing-for-your-app)."
#### Verified Apps
1. Add a pricing plan. For more information, see "[Setting pricing plans for your listing](/developers/github-marketplace/setting-pricing-plans-for-your-listing)."
If you've already built an app and you're interested in submitting a verified listing in {% data variables.product.prodname_marketplace %}, start here:
1. Check whether your app meets the requirements for listing on {% data variables.product.prodname_marketplace %} as a free or a paid app. For more information, see "[Requirements for listing an app](/developers/github-marketplace/requirements-for-listing-an-app)."
1. [Getting started with {% data variables.product.prodname_marketplace %}](/marketplace/getting-started/)<br/>Learn about requirements, guidelines, and the app submission process.
1. Read and accept the terms of the "[{% data variables.product.prodname_marketplace %} Developer Agreement](/articles/github-marketplace-developer-agreement/)."
1. [Integrating with the {% data variables.product.prodname_marketplace %} API](/marketplace/integrating-with-the-github-marketplace-api/)<br/>Before you can list your app on {% data variables.product.prodname_marketplace %}, you'll need to integrate billing flows using the {% data variables.product.prodname_marketplace %} API and webhook events.
1. Submit your listing for publication in {% data variables.product.prodname_marketplace %}, requesting verification if you want to sell the app. For more information, see "[Submitting your listing for publication](/developers/github-marketplace/submitting-your-listing-for-publication)."
1. [Listing on {% data variables.product.prodname_marketplace %}](/marketplace/listing-on-github-marketplace/) <br/>Create a draft {% data variables.product.prodname_marketplace %} listing, configure webhook settings, and set up pricing plans.
An onboarding expert will contact you with any questions or further steps. For example, if you have added a paid plan, you will need to complete the verification process and complete financial onboarding. As soon as your listing is approved the app is published to {% data variables.product.prodname_marketplace %}.
1. [Selling your app](/marketplace/selling-your-app/)<br/>Learn about pricing plans, billing cycles, and how to receive payment from {% data variables.product.prodname_dotcom %} for your app.
### Seeing how your app is performing
1. [{% data variables.product.prodname_marketplace %} Insights](/marketplace/github-marketplace-insights/)<br/>See how your app is performing in {% data variables.product.prodname_marketplace %}. You can use metrics collected by {% data variables.product.prodname_dotcom %} to guide your marketing campaign and be successful in {% data variables.product.prodname_marketplace %}.
You can access metrics and transactions for your listing. For more information, see:
1. [{% data variables.product.prodname_marketplace %} transactions](/marketplace/github-marketplace-transactions/)<br/>Download and view transaction data for your {% data variables.product.prodname_marketplace %} listing.
### Reviewing your app
We want to make sure that the apps offered on {% data variables.product.prodname_marketplace %} are safe, secure, and well tested. The {% data variables.product.prodname_marketplace %} onboarding specialists will review your app to ensure that it meets all requirements. Follow the guidelines in these articles before submitting your app:
* [Requirements for listing an app on {% data variables.product.prodname_marketplace %}](/marketplace/getting-started/requirements-for-listing-an-app-on-github-marketplace/)
* [Security review process](/marketplace/getting-started/security-review-process/)
- "[Viewing metrics for your listing](/developers/github-marketplace/viewing-metrics-for-your-listing)"
- "[Viewing transactions for your listing](/developers/github-marketplace/viewing-transactions-for-your-listing)"

Просмотреть файл

@ -0,0 +1,43 @@
---
title: About verified creators
intro: 'Each organization that wants to sell apps on {% data variables.product.prodname_marketplace %} must follow a verification process. Their identity is checked and their billing process reviewed.'
versions:
free-pro-team: '*'
---
### About verified creators
A verified creator is an organization that {% data variables.product.company_short %} has checked. Anyone can share their apps with other users on {% data variables.product.prodname_marketplace %} but only organizations that are verified by {% data variables.product.company_short %} can sell apps. For more information about organizations, see "[About organizations](/github/setting-up-and-managing-organizations-and-teams/about-organizations)."
The verification process aims to protect users. For example, it verifies the seller's identity, checks that their {% data variables.product.product_name %} organization is set up securely, and that they can be contacted for support.
After passing the verification checks, any apps that the organization lists on {% data variables.product.prodname_marketplace %} are shown with a verified creator badge {% octicon "verified" aria-label="Verified creator badge" %}. The organization can now add paid plans to any of their apps. Each app with a paid plan also goes through a financial onboarding process to check that it's set up to handle billing correctly.
![verified creator badges](/assets/images/marketplace/marketplace_verified_creator_badges_apps.png)
In addition to the verified creator badge, you'll also see badges for unverified and verified apps. These apps were published using the old method for verifying individual apps.
![Green verified and grey unverified badge](/assets/images/marketplace/marketplace_verified_badges.png)
For information on finding apps to use, see "[Searching {% data variables.product.prodname_marketplace %}](/github/searching-for-information-on-github/searching-github-marketplace)."
### About the verification process
The first time you request verification for a listing of one of your apps, you will enter the verification process. An onboarding expert will guide you through the process. This includes checking:
- Profile information - The basic profile information is populated accurately and appropriately.
- Security - The organization has enabled two-factor authentication.
- Verified domain - The organization has verified the domain of the site URL.
- Purchase webhook event - The event is handled correctly by the app.
When your organization is verified, all your apps are shown with a verified creator badge. You are now able to offer paid plans for any of your apps.
For more information about the requirements for listing an app on {% data variables.product.prodname_marketplace %}, see "[Requirements for listing an app on {% data variables.product.prodname_marketplace %}](/marketplace/getting-started/requirements-for-listing-an-app-on-github-marketplace/)."
{% data reusables.marketplace.app-transfer-to-org-for-verification %} For information on how to do this, see: "[Submitting your listing for publication](/developers/github-marketplace/submitting-your-listing-for-publication#transferring-an-app-to-an-organization-before-you-submit)."
{% note %}
**Note:** This verification process for apps replaces the previous process where individual apps were verified. The current process is similar to the verification process for actions. If you have apps that were verified under the old process, these will not be affected by the changes. The {% data variables.product.prodname_marketplace %} team will contact you with details of how to migrate to organization-based verification.
{% endnote %}

Просмотреть файл

@ -13,17 +13,17 @@ versions:
### Understanding the billing cycle
Customers can choose a monthly or yearly billing cycle when they purchase your app. All changes customers make to the billing cycle and plan selection will trigger a `marketplace_purchase` event. You can refer to the `marketplace_purchase` webhook payload to see which billing cycle a customer selects and when the next billing date begins (`effective_date`). For more information about webhook payloads, see "[{% data variables.product.prodname_marketplace %} webhook events](/marketplace/integrating-with-the-github-marketplace-api/github-marketplace-webhook-events/)."
Customers can choose a monthly or yearly billing cycle when they purchase your app. All changes customers make to the billing cycle and plan selection will trigger a `marketplace_purchase` event. You can refer to the `marketplace_purchase` webhook payload to see which billing cycle a customer selects and when the next billing date begins (`effective_date`). For more information about webhook payloads, see "[Webhook events for the {% data variables.product.prodname_marketplace %} API](/developers/github-marketplace/webhook-events-for-the-github-marketplace-api)."
### Providing billing services in your app's UI
Customers must be able to perform the following actions from your app's website:
- Customers must be able to modify or cancel their {% data variables.product.prodname_marketplace %} plans for personal and organizational accounts separately.
Customers should be able to perform the following actions from your app's website:
- Customers should be able to modify or cancel their {% data variables.product.prodname_marketplace %} plans for personal and organizational accounts separately.
{% data reusables.marketplace.marketplace-billing-ui-requirements %}
### Billing services for upgrades, downgrades, and cancellations
Follow these guidelines for upgrades, downgrades, and cancellations to maintain a clear and consistent billing process. For more detailed instructions about the {% data variables.product.prodname_marketplace %} purchase events, see "[Billing flows](/marketplace/integrating-with-the-github-marketplace-api/#billing-flows)."
Follow these guidelines for upgrades, downgrades, and cancellations to maintain a clear and consistent billing process. For more detailed instructions about the {% data variables.product.prodname_marketplace %} purchase events, see "[Using the {% data variables.product.prodname_marketplace %} API in your app](/developers/github-marketplace/using-the-github-marketplace-api-in-your-app)."
You can use the `marketplace_purchase` webhook's `effective_date` key to determine when a plan change will occur and periodically synchronize the [List accounts for a plan](/rest/reference/apps#list-accounts-for-a-plan).
@ -33,7 +33,7 @@ When a customer upgrades their pricing plan or changes their billing cycle from
{% data reusables.marketplace.marketplace-failed-purchase-event %}
For information about building upgrade and downgrade workflows into your app, see "[Upgrading and downgrading plans](/marketplace/integrating-with-the-github-marketplace-api/upgrading-and-downgrading-plans/)."
For information about building upgrade and downgrade workflows into your app, see "[Handling plan changes](/developers/github-marketplace/handling-plan-changes)."
#### Downgrades and cancellations
@ -45,4 +45,4 @@ When a customer cancels a plan, you must:
{% data reusables.marketplace.cancellation-clarification %}
- Enable them to upgrade the plan through GitHub if they would like to continue the plan at a later time.
For information about building cancellation workflows into your app, see "[Cancelling plans](/marketplace/integrating-with-the-github-marketplace-api/cancelling-plans/)."
For information about building cancellation workflows into your app, see "[Handling plan cancellations](/developers/github-marketplace/handling-plan-cancellations)."

Просмотреть файл

@ -0,0 +1,20 @@
---
title: Customer experience best practices for apps
intro: 'Guidelines for creating an app that will be easy to use and understand.'
shortTitle: Customer experience best practice
versions:
free-pro-team: '*'
---
If you follow these best practices it will help you to provide a good customer experience.
### Customer communication
- Marketing materials for the app should accurately represent the app's behavior.
- Apps should include links to user-facing documentation that describe how to set up and use the app.
- Customers should be able to see what type of plan they have in the billing, profile, or account settings section of the app.
- Customers should be able to install and use your app on both a personal account and an organization account. They should be able to view and manage the app on those accounts separately.
### Plan management
{% data reusables.marketplace.marketplace-billing-ui-requirements %}

Просмотреть файл

@ -59,8 +59,8 @@ Once you've created a {% data variables.product.prodname_marketplace %} draft li
### Submitting your app
Once you've completed your {% data variables.product.prodname_marketplace %} listing, you can submit your listing for review from the **Overview** page. You'll need to read and accept the "[{% data variables.product.prodname_marketplace %} Developer Agreement](/articles/github-marketplace-developer-agreement/)," and then you can click **Submit for review**. After you submit your app for review, the {% data variables.product.prodname_marketplace %} onboarding team will contact you with additional information about the onboarding process. You can learn more about the onboarding and security review process in "[Getting started with {% data variables.product.prodname_marketplace %}](/marketplace/getting-started/)."
Once you've completed your {% data variables.product.prodname_marketplace %} listing, you can submit your listing for review from the **Overview** page. You'll need to read and accept the "[{% data variables.product.prodname_marketplace %} Developer Agreement](/articles/github-marketplace-developer-agreement/)," and then you can click **Submit for review**. After you submit your app for review, an onboarding expert will contact you with additional information about the onboarding process. You can learn more about the onboarding and security review process in "[Getting started with {% data variables.product.prodname_marketplace %}](/marketplace/getting-started/)."
### Removing a {% data variables.product.prodname_marketplace %} listing
If you no longer want to list your app in {% data variables.product.prodname_marketplace %}, contact [marketplace@github.com](mailto:marketplace@github.com) to remove your listing.
If you no longer want to list your app in {% data variables.product.prodname_marketplace %}, contact {% data variables.contact.contact_support %} to remove your listing.

Просмотреть файл

@ -28,7 +28,7 @@ GitHub then sends the [`marketplace_purchase`](/webhooks/event-payloads/#marketp
Read the `effective_date` and `marketplace_purchase` object from the `marketplace_purchase` webhook to determine which plan the customer purchased, when the billing cycle starts, and when the next billing cycle begins.
If your app offers a free trial, read the `marketplace_purchase[on_free_trial]` attribute from the webhook. If the value is `true`, your app will need to track the free trial start date (`effective_date`) and the date the free trial ends (`free_trial_ends_on`). Use the `free_trial_ends_on` date to display the remaining days left in a free trial in your app's UI. You can do this in either a banner or in your [billing UI](/marketplace/selling-your-app/billing-customers-in-github-marketplace/#providing-billing-services-in-your-apps-ui). To learn how to handle cancellations before a free trial ends, see "[Cancelling plans](/marketplace/integrating-with-the-github-marketplace-api/cancelling-plans/)." See "[Upgrading and downgrading plans](/marketplace/integrating-with-the-github-marketplace-api/upgrading-and-downgrading-plans/)" to find out how to transition a free trial to a paid plan when a free trial expires.
If your app offers a free trial, read the `marketplace_purchase[on_free_trial]` attribute from the webhook. If the value is `true`, your app will need to track the free trial start date (`effective_date`) and the date the free trial ends (`free_trial_ends_on`). Use the `free_trial_ends_on` date to display the remaining days left in a free trial in your app's UI. You can do this in either a banner or in your [billing UI](/marketplace/selling-your-app/billing-customers-in-github-marketplace/#providing-billing-services-in-your-apps-ui). To learn how to handle cancellations before a free trial ends, see "[Handling plan cancellations](/developers/github-marketplace/handling-plan-cancellations)." See "[Handling plan changes](/developers/github-marketplace/handling-plan-changes)" to find out how to transition a free trial to a paid plan when a free trial expires.
See "[{% data variables.product.prodname_marketplace %} webhook events](/marketplace/integrating-with-the-github-marketplace-api/github-marketplace-webhook-events/)" for an example of the `marketplace_purchase` event payload.

Просмотреть файл

@ -11,8 +11,10 @@ versions:
{% topic_link_in_list /creating-apps-for-github-marketplace %}
{% link_in_list /about-github-marketplace %}
{% link_in_list /about-verified-creators %}
{% link_in_list /requirements-for-listing-an-app %}
{% link_in_list /security-review-process-for-submitted-apps %}
{% link_in_list /security-best-practices-for-apps %}
{% link_in_list /customer-experience-best-practices-for-apps %}
{% link_in_list /viewing-metrics-for-your-listing %}
{% link_in_list /viewing-transactions-for-your-listing %}
{% topic_link_in_list /using-the-github-marketplace-api-in-your-app %}
@ -27,7 +29,7 @@ versions:
{% link_in_list /writing-a-listing-description-for-your-app %}
{% link_in_list /setting-pricing-plans-for-your-listing %}
{% link_in_list /configuring-a-webhook-to-notify-you-of-plan-changes %}
{% link_in_list /submitting-your-listing-for-review %}
{% link_in_list /submitting-your-listing-for-publication %}
{% topic_link_in_list /selling-your-app-on-github-marketplace %}
{% link_in_list /pricing-plans-for-github-marketplace-apps %}
{% link_in_list /billing-customers %}

Просмотреть файл

@ -10,35 +10,45 @@ versions:
{% data variables.product.prodname_marketplace %} pricing plans can be free, flat rate, or per-unit, and GitHub lists the price in US dollars. Customers purchase your app using a payment method attached to their {% data variables.product.product_name %} account, without having to leave GitHub.com. You don't have to write code to perform billing transactions, but you will have to handle [billing flows](/marketplace/integrating-with-the-github-marketplace-api/#billing-flows) for purchase events.
{% data variables.product.prodname_marketplace %} pricing plans can be free, flat rate, or per-unit. Prices are set, displayed, and processed in US dollars. Paid plans are restricted to verified listings.
Customers purchase your app using a payment method attached to their {% data variables.product.product_name %} account, without having to leave {% data variables.product.prodname_dotcom_the_website %}. You don't have to write code to perform billing transactions, but you will have to handle events from the {% data variables.product.prodname_marketplace %} API. For more information, see "[Using the {% data variables.product.prodname_marketplace %} API in your app](/developers/github-marketplace/using-the-github-marketplace-api-in-your-app)."
If the app you're listing on {% data variables.product.prodname_marketplace %} has multiple plan options, you can set up corresponding pricing plans. For example, if your app has two plan options, an open source plan and a pro plan, you can set up a free pricing plan for your open source plan and a flat pricing plan for your pro plan. Each {% data variables.product.prodname_marketplace %} listing must have an annual and a monthly price for every plan that's listed.
For more information on how to create a pricing plan, see "[Setting a {% data variables.product.prodname_marketplace %} listing's pricing plan](/marketplace/listing-on-github-marketplace/setting-a-github-marketplace-listing-s-pricing-plan/)."
{% note %}
**Note:** If you're listing an app on {% data variables.product.prodname_marketplace %}, you can't list your app with a free pricing plan if you offer a paid service outside of {% data variables.product.prodname_marketplace %}.
{% endnote %}
{% data reusables.marketplace.free-plan-note %}
### Types of pricing plans
**Free pricing plans** are completely free for users. If you set up a free pricing plan, you cannot charge users that choose the free pricing plan for the use of your app. You can create both free and paid plans for your listing. Unverified free apps do not need to implement any billing flows. Free apps that are verified by Github need to implement billing flows for new purchases and cancellations, but do not need to implement billing flows for free trials, upgrades, and downgrades. If you add a paid plan to an app that you've already listed in {% data variables.product.prodname_marketplace %} as a free service, you'll need to resubmit the app for review.
#### Free pricing plans
**Flat rate pricing plans** charge a set fee on a monthly and yearly basis.
{% data reusables.marketplace.free-apps-encouraged %}
**Per-unit pricing plans** charge a set fee on either a monthly or yearly basis for a unit that you specify. A "unit" can be anything you'd like (for example, a user, seat, or person).
Free plans are completely free for users. If you set up a free pricing plan, you cannot charge users that choose the free pricing plan for the use of your app. You can create both free and paid plans for your listing.
**Marketplace free trials** provide 14-day free trials of OAuth or GitHub Apps to customers. When you [set up a Marketplace pricing plan](/marketplace/listing-on-github-marketplace/setting-a-github-marketplace-listing-s-pricing-plan/), you can select the option to provide a free trial for flat-rate or per-unit pricing plans.
All apps need to handle events for new purchases and cancellations. Apps that only have free plans do not need to handle events for free trials, upgrades, and downgrades. For more information, see: "[Using the {% data variables.product.prodname_marketplace %} API in your app](/developers/github-marketplace/using-the-github-marketplace-api-in-your-app)."
If you add a paid plan to an app that you've already listed in {% data variables.product.prodname_marketplace %} as a free service, you'll need to request verification for the app and go through financial onboarding.
#### Paid pricing plans
There are two types of paid pricing plan:
- Flat rate pricing plans charge a set fee on a monthly and yearly basis.
- Per-unit pricing plans charge a set fee on either a monthly or yearly basis for a unit that you specify. A "unit" can be anything you'd like (for example, a user, seat, or person).
You may also want to offer free trials. These provide free, 14-day trials of OAuth or GitHub Apps to customers. When you set up a Marketplace pricing plan, you can select the option to provide a free trial for flat-rate or per-unit pricing plans.
### Free trials
Customers can start a free trial for any available paid plan on a Marketplace listing, but will not be able to create more than one free trial for a Marketplace product.
Customers can start a free trial for any paid plan on a Marketplace listing that includes free trials. However, customers cannot create more than one free trial per marketplace product.
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.
See "[New purchases and free trials](/marketplace/integrating-with-the-github-marketplace-api/handling-new-purchases-and-free-trials/)" for details on how to handle free trials in your app.
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/)."
{% note %}

Просмотреть файл

@ -1,6 +1,6 @@
---
title: Requirements for listing an app
intro: 'Apps on {% data variables.product.prodname_marketplace %} must meet the requirements outlined on this page before our {% data variables.product.prodname_marketplace %} onboarding specialists will approve the listing.'
intro: 'Apps on {% data variables.product.prodname_marketplace %} must meet the requirements outlined on this page before the listing can be published.'
redirect_from:
- /apps/adding-integrations/listing-apps-on-github-marketplace/requirements-for-listing-an-app-on-github-marketplace/
- /apps/marketplace/listing-apps-on-github-marketplace/requirements-for-listing-an-app-on-github-marketplace/
@ -12,49 +12,62 @@ versions:
free-pro-team: '*'
---
<!--UI-LINK: Displayed as a link on the https://github.com/marketplace/new page.-->
The requirements for listing an app on {% data variables.product.prodname_marketplace %} vary according to whether you want to offer a free or a paid app.
Before you submit your app for review, you must read and accept the terms of the "[{% data variables.product.prodname_marketplace %} Developer Agreement](/articles/github-marketplace-developer-agreement/)." You'll accept the terms within your [draft listing](/marketplace/listing-on-github-marketplace/creating-a-draft-github-marketplace-listing/) on {% data variables.product.product_name %}. Once you've submitted your app, one of the {% data variables.product.prodname_marketplace %} onboarding specialists will reach out to you with more information about the onboarding process, and review your app to ensure it meets these requirements:
### Requirements for all {% data variables.product.prodname_marketplace %} listings
### User experience
All listings on {% data variables.product.prodname_marketplace %} should be for tools that provide value to the {% data variables.product.product_name %} community. When you submit your listing for publication, you must read and accept the terms of the "[{% data variables.product.prodname_marketplace %} Developer Agreement](/articles/github-marketplace-developer-agreement/)."
- {% 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.
#### User experience requirements for all apps
All listings should meet the following requirements, regardless of whether they are for a free or paid app.
- Listings must not actively persuade users away from {% data variables.product.product_name %}.
- Listings must include valid contact information for the publisher.
- Listings must have a relevant description of the application.
- Listings must specify a pricing plan.
- Apps must provide value to customers and integrate with the platform in some way beyond authentication.
- Apps must be publicly available in {% data variables.product.prodname_marketplace %} and cannot be in beta or available by invite only.
- Apps cannot actively persuade users away from {% data variables.product.product_name %}.
- Marketing materials for the app must accurately represent the app's behavior.
- Apps must include links to user-facing documentation that describe how to set up and use the app.
- When a customer purchases an app and GitHub redirects them to the app's installation URL, the app must begin the OAuth flow immediately. For details, see "[Handling new purchases and free trials](/marketplace/integrating-with-the-github-marketplace-api/handling-new-purchases-and-free-trials/#step-3-authorization)."
- Apps must have webhook events set up to notify the publisher of any plan changes or cancellations using the {% data variables.product.prodname_marketplace %} API. For more information, see "[Using the {% data variables.product.prodname_marketplace %} API in your app](/developers/github-marketplace/using-the-github-marketplace-api-in-your-app)."
- Customers must be able to install your app and select repositories on both a personal and organization account. They should be able to view and manage those accounts separately.
For more information on providing a good customer experience, see "[Customer experience best practices for apps](/developers/github-marketplace/customer-experience-best-practices-for-apps)."
### Brand and listing
#### Brand and listing requirements for all apps
- Apps that use GitHub logos must follow the "[{% data variables.product.product_name %} Logos and Usage](https://github.com/logos)" guidelines.
- Apps that use GitHub logos must follow the {% data variables.product.company_short %} guidelines. For more information, see "[{% data variables.product.company_short %} Logos and Usage](https://github.com/logos)."
- Apps must have a logo, feature card, and screenshots images that meet the recommendations provided in "[Writing {% data variables.product.prodname_marketplace %} listing descriptions](/marketplace/listing-on-github-marketplace/writing-github-marketplace-listing-descriptions/)."
- Listings must include descriptions that are well written and free of grammatical errors. For guidance in writing your listing, see "[Writing {% data variables.product.prodname_marketplace %} listing descriptions](/marketplace/listing-on-github-marketplace/writing-github-marketplace-listing-descriptions/)."
### Security
To protect your customers, we recommend that you also follow security best practices. For more information, see "[Security best practices for apps](/developers/github-marketplace/security-best-practices-for-apps)."
Apps will go through a security review before being listed on {% data variables.product.prodname_marketplace %}. A successful review will meet the requirements and follow the security best practices listed in "[Security review process](/marketplace/getting-started/security-review-process/)." For information on the review process, contact [marketplace@github.com](mailto:marketplace@github.com).
### Considerations for free apps
### Billing flows
{% data reusables.marketplace.free-apps-encouraged %}
Your app must integrate [billing flows](/marketplace/integrating-with-the-github-marketplace-api/#billing-flows) using the [{% data variables.product.prodname_marketplace %} webhook event](/marketplace/integrating-with-the-github-marketplace-api/github-marketplace-webhook-events/).
### Requirements for paid apps
#### Free apps
In addition to the requirements for all apps above, each app that you offer as a paid service on {% data variables.product.prodname_marketplace %} must also meet the following requirements:
{% data reusables.marketplace.free-apps-encouraged %} If you are listing a free app, you'll need to meet these requirements:
- {% 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.")
- Customers must be able to see that they have a free plan in the billing, profile, or account settings section of the app.
- When a customer cancels your app, you must follow the flow for [cancelling plans](/marketplace/integrating-with-the-github-marketplace-api/cancelling-plans/).
When you are ready to publish the app on {% data variables.product.prodname_marketplace %} you must request verification for the listing.
#### Paid apps
{% note %}
To offer your app as a paid service, you'll need to meet these requirements to list your app on {% data variables.product.prodname_marketplace %}:
The verification process is open to organizations. {% data reusables.marketplace.app-transfer-to-org-for-verification %} For information on how to do this, see: "[Submitting your listing for publication](/developers/github-marketplace/submitting-your-listing-for-publication#transferring-an-app-to-an-organization-before-you-submit)."
{% endnote %}
### Billing requirements for paid apps
Your app does not need to handle payments but does need to use {% data variables.product.prodname_marketplace %} purchase events to manage new purchases, upgrades, downgrades, cancellations, and free trials. For information about how integrate these events into your app, see "[Using the {% data variables.product.prodname_marketplace %} API in your app](/developers/github-marketplace/using-the-github-marketplace-api-in-your-app)."
Using GitHub's billing API allows customers to purchase an app without leaving GitHub and to pay for the service with the payment method already attached to their {% data variables.product.product_name %} account.
- To sell your app in {% data variables.product.prodname_marketplace %}, it must use GitHub's billing system. Your app does not need to handle payments but does need to use "[{% data variables.product.prodname_marketplace %} purchase events](/marketplace/integrating-with-the-github-marketplace-api/github-marketplace-webhook-events/)" to manage new purchases, upgrades, downgrades, cancellations, and free trials. See "[Billing flows](/marketplace/integrating-with-the-github-marketplace-api/#billing-flows)" to learn about how to integrate these events into your app. Using GitHub's billing system allows customers to purchase an app without leaving GitHub and pay for the service with the payment method already attached to their {% data variables.product.product_name %} account.
- Apps must support both monthly and annual billing for paid subscriptions purchases.
- Listings may offer any combination of free and paid plans. Free plans are optional but encouraged. For more information, see "[Setting a {% data variables.product.prodname_marketplace %} listing's pricing plan](/marketplace/listing-on-github-marketplace/setting-a-github-marketplace-listing-s-pricing-plan/)."
{% data reusables.marketplace.marketplace-billing-ui-requirements %}

Просмотреть файл

@ -0,0 +1,60 @@
---
title: Security best practices for apps
intro: 'Guidelines for preparing a secure app to share on {% data variables.product.prodname_marketplace %}.'
redirect_from:
- /apps/marketplace/getting-started/security-review-process/
- /marketplace/getting-started/security-review-process
- /developers/github-marketplace/security-review-process-for-submitted-apps
shortTitle: Security best practice
versions:
free-pro-team: '*'
---
If you follow these best practices it will help you to provide a secure user experience.
### Authorization, authentication, and access control
We recommend creating a GitHub App rather than an OAuth App. {% data reusables.marketplace.github_apps_preferred %}. See "[Differences between GitHub Apps and OAuth Apps](/apps/differences-between-apps/)" for more details.
- Apps should use the principle of least privilege and should only request the OAuth scopes and GitHub App permissions that the app needs to perform its intended functionality. For more information, see [Principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) in Wikipedia.
- Apps should provide customers with a way to delete their account, without having to email or call a support person.
- Apps should not share tokens between different implementations of the app. For example, a desktop app should have a separate token from a web-based app. Individual tokens allow each app to request the access needed for GitHub resources separately.
- Design your app with different user roles, depending on the functionality needed by each type of user. For example, a standard user should not have access to admin functionality, and billing managers might not need push access to repository code.
- Apps should not share service accounts such as email or database services to manage your SaaS service.
- All services used in your app should have unique login and password credentials.
- Admin privilege access to the production hosting infrastructure should only be given to engineers and employees with administrative duties.
- Apps should not use personal access tokens to authenticate and should authenticate as an [OAuth App](/apps/about-apps/#about-oauth-apps) or a [GitHub App](/apps/about-apps/#about-github-apps):
- OAuth Apps should authenticate using an [OAuth token](/apps/building-oauth-apps/authorizing-oauth-apps/).
- GitHub Apps should authenticate using either a [JSON Web Token (JWT)](/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app), [OAuth token](/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps/), or [installation access token](/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-an-installation).
### Data protection
- Apps should encrypt data transferred over the public internet using HTTPS, with a valid TLS certificate, or SSH for Git.
- Apps should store client ID and client secret keys securely. We recommend storing them as [environmental variables](http://en.wikipedia.org/wiki/Environment_variable#Getting_and_setting_environment_variables).
- Apps should delete all GitHub user data within 30 days of receiving a request from the user, or within 30 days of the end of the user's legal relationship with GitHub.
- Apps should not require the user to provide their GitHub password.
- Apps should encrypt tokens, client IDs, and client secrets.
### Logging and monitoring
Apps should have logging and monitoring capabilities. App logs should be retained for at least 30 days and archived for at least one year.
A security log should include:
- Authentication and authorization events
- Service configuration changes
- Object reads and writes
- All user and group permission changes
- Elevation of role to admin
- Consistent timestamping for each event
- Source users, IP addresses, and/or hostnames for all logged actions
### Incident response workflow
To provide a secure experience for users, you should have a clear incident response plan in place before listing your app. We recommend having a security and operations incident response team in your company rather than using a third-party vendor. You should have the capability to notify {% data variables.product.product_name %} within 24 hours of a confirmed incident.
For an example of an incident response workflow, see the "Data Breach Response Policy" on the [SANS Institute website](https://www.sans.org/information-security-policy/). A short document with clear steps to take in the event of an incident is more valuable than a lengthy policy template.
### Vulnerability management and patching workflow
You should conduct regular vulnerability scans of production infrastructure. You should triage the results of vulnerability scans and define a period of time in which you agree to remediate the vulnerability.
If you are not ready to set up a full vulnerability management program, it's useful to start by creating a patching process. For guidance in creating a patch management policy, see this TechRepublic article "[Establish a patch management policy](https://www.techrepublic.com/blog/it-security/establish-a-patch-management-policy-87756/)."

Просмотреть файл

@ -1,94 +0,0 @@
---
title: Security review process for submitted apps
intro: 'GitHub''s security team reviews all apps submitted to {% data variables.product.prodname_marketplace %} to ensure that they meet security requirements. Follow these best practices to be prepared for the review process.'
redirect_from:
- /apps/marketplace/getting-started/security-review-process/
- /marketplace/getting-started/security-review-process
versions:
free-pro-team: '*'
---
After you've submitted your app for approval, the GitHub security team will request that you complete a security questionnaire about your app and overall security program. As part of the review, you will have the option to provide documentation to support your responses. You must submit two required documents before your app will be approved for {% data variables.product.prodname_marketplace %}: an [incident response plan](#incident-response-plan) and [vulnerability management workflow](#vulnerability-management-workflow).
### Security best practices
Follow these best practices to have a successful security review and provide a secure user experience.
#### Authorization, authentication, and access control
We recommend submitting a GitHub App rather than an OAuth App. {% data reusables.marketplace.github_apps_preferred %}. See "[Differences between GitHub Apps and OAuth Apps](/apps/differences-between-apps/)" for more details.
- Apps must use the "[principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege)" and should only request the OAuth scopes and GitHub App permissions that the app needs to perform its intended functionality.
- Apps must provide customers with a way to delete their account, without having to email or call a support person.
- Apps should not share tokens between different implementations of the app. For example, a desktop app should have a separate token from a web-based app. Individual tokens allow each app to request the access needed for GitHub resources separately.
- Design your app with different user roles, depending on the functionality needed by each type of user. For example, a standard user should not have access to admin functionality, and billing managers might not need push access to repository code.
- Your app should not share service accounts such as email or database services to manage your SaaS service.
- All services used in your app should have unique login and password credentials.
- Admin privilege access to the production hosting infrastructure should only be given to engineers and employees with administrative duties.
- Apps cannot use personal access tokens to authenticate and must authenticate as an [OAuth App](/apps/about-apps/#about-oauth-apps) or [GitHub App](/apps/about-apps/#about-github-apps):
- OAuth Apps must authenticate using an [OAuth token](/apps/building-oauth-apps/authorizing-oauth-apps/).
- GitHub Apps must authenticate using either a [JSON Web Token (JWT)](/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app), [OAuth token](/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps/), or [installation access token](/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-an-installation).
#### Data protection
- Apps must encrypt data transferred over the public internet using HTTPS, with a valid TLS certificate, or SSH for Git.
- Apps must store client ID and client secret keys securely. We recommend storing them as [environmental variables](http://en.wikipedia.org/wiki/Environment_variable#Getting_and_setting_environment_variables).
- Apps must delete all GitHub user data within 30 days of receiving a request from the user, or within 30 days of the end of the user's legal relationship with GitHub.
- Apps cannot require the user to provide their GitHub password.
- Apps should encrypt tokens, client IDs, and client secrets.
#### Logging and monitoring
- Apps must have logging and monitoring capabilities. App logs must be retained for at least 30 days and archived for at least one year.
A security log should include:
- Authentication and authorization events
- Service configuration changes
- Object reads and writes
- All user and group permission changes
- Elevation of role to admin
- Consistent timestamping for each event
- Source users, IP addresses, and/or hostnames for all logged actions
#### Incident response workflow
- To partner with GitHub, you are required to have an [incident response plan](#incident-response-plan) in place before submitting your {% data variables.product.prodname_marketplace %} app listing.
- We recommend having a security and operations incident response team in your company rather than using a third-party vendor.
- You should have the capability to notify GitHub within 24 hours of a confirmed incident.
- You should familiarize yourself with sections 3.7.5 - 3.7.5.6 of the [{% data variables.product.prodname_marketplace %} Developer Agreement](/github/site-policy/github-marketplace-developer-agreement#3-restrictions-and-responsibilities), which include additional details on incident response workflow requirements.
#### Vulnerability management and patching workflow
- You should conduct regular vulnerability scans of production infrastructure.
- You should triage the results of vulnerability scans and define a period of time in which you agree to remediate the vulnerability.
- You should familiarize yourself with section 3.7.3 of the [{% data variables.product.prodname_marketplace %} Developer Agreement](/github/site-policy/github-marketplace-developer-agreement#3-restrictions-and-responsibilities), which includes additional details on vulnerability management and patching workflows requirements.
### Security program documentation
During the Marketplace security review, you will be asked to submit your incident response plan and vulnerability management workflow. Each document must include a company-branded statement signed by management with a date stamp.
#### Incident response plan
Your incident response plan documentation must include the current process that your company follows, who is accountable, and the person to contact or expect contact from if an incident occurs. The "[NIST Computer Security Incident Handling Guide](http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-61r2.pdf)" is a great example of a document that covers incident response in general. Section 2.3 "Incident Response Policy, Plan, and Procedure Creation" specifically covers the policy. Another great example is the "[SANS Data Breach Response Policy](https://www.sans.org/security-resources/policies/general/pdf/data-breach-response)."
#### Vulnerability management workflow
Your vulnerability management workflow documentation must include the current process that your company follows for vulnerability management and the patching process used. If you don't have a full vulnerability management program, it might help to start by creating a patching process. For guidance in creating a patch management policy, read the article "[Establish a patch management policy](https://www.techrepublic.com/blog/it-security/establish-a-patch-management-policy-87756/)."
{% note %}
**Note:** The incident response and vulnerability management workflow documents aren't expected to be massive formal policy or program documents. A page or two about what you do is more valuable than a lengthy policy template.
{% endnote %}
#### GitHub Marketplace security program questionnaire
During the app submission process, our {% data variables.product.prodname_marketplace %} onboarding team will also send you a questionnaire requesting information about your security practices. This document will serve as a written record attesting:
- The authentication method and scopes required by your app.
- That you're not requesting more scopes or {% data variables.product.product_name %} access than is needed for the app to perform its intended functionality, taking OAuth limitations and use of {% data variables.product.prodname_github_app %}s into account.
- The use of any third-party services or infrastructure, such as SaaS, PaaS, or IaaS.
- An incident response procedure exists.
- Your app's method of key/token handling.
- That a responsible disclosure policy and process in place or plans to implement one within six months.
- Your vulnerability management workflow or program.
- That you have logging and monitoring capabilities. You must also provide evidence that any relevant app logs are retained for at least 30 days and archived for at least one year.

Просмотреть файл

@ -1,6 +1,6 @@
---
title: Setting pricing plans for your listing
intro: 'When [listing your app on {% data variables.product.prodname_marketplace %}](/marketplace/listing-on-github-marketplace/), you can choose to provide your app as a free service or sell your app. If you plan to sell your app, you can create different pricing plans for different feature tiers.'
intro: 'When you list your app on {% data variables.product.prodname_marketplace %}, you can choose to provide your app as a free service or sell your app. If you plan to sell your app, you can create different pricing plans for different feature tiers.'
redirect_from:
- /apps/adding-integrations/managing-pricing-and-payments-for-a-github-marketplace-listing/setting-a-github-marketplace-listing-s-pricing-plan/
- /apps/marketplace/managing-pricing-and-payments-for-a-github-marketplace-listing/setting-a-github-marketplace-listing-s-pricing-plan/
@ -17,57 +17,52 @@ versions:
free-pro-team: '*'
---
### About setting pricing plans
If you want to sell an app on {% data variables.product.prodname_marketplace %}, you need to request verification when you publish the listing for your app. During the verification process, an onboarding expert checks the organization's identity and security settings. The onboarding expert will also take the organization through financial onboarding. For more information, see: "[Requirements for listing an app on {% data variables.product.prodname_marketplace %}](/marketplace/getting-started/requirements-for-listing-an-app-on-github-marketplace/)."
{% data reusables.marketplace.app-transfer-to-org-for-verification %} For information on how to do this, see: "[Submitting your listing for publication](/developers/github-marketplace/submitting-your-listing-for-publication#transferring-an-app-to-an-organization-before-you-submit)."
{% data variables.product.prodname_marketplace %} offers several different types of pricing plan. For detailed information, see "[Pricing plans for {% data variables.product.prodname_marketplace %}](/developers/github-marketplace/pricing-plans-for-github-marketplace-apps)."
### About saving pricing plans
You can save pricing plans in a draft or published state. If you haven't submitted your {% data variables.product.prodname_marketplace %} listing for approval, a published plan will function in the same way as a draft plan until your listing is approved and shown on {% data variables.product.prodname_marketplace %}. Draft plans allow you to create and save new pricing plans without making them available on your {% data variables.product.prodname_marketplace %} listing page. Once you publish a pricing plan on a published listing, it's available for customers to purchase immediately. You can publish up to 10 pricing plans.
For guidelines on billing customers, see "[Billing customers](/developers/github-marketplace/billing-customers)."
### Creating pricing plans
To learn about the types of pricing plans that {% data variables.product.prodname_marketplace %} offers, see "[{% data variables.product.prodname_marketplace %} Pricing Plans](/marketplace/selling-your-app/github-marketplace-pricing-plans/)." You'll also find helpful billing guidelines in "[Selling your app](/marketplace/selling-your-app/)."
Pricing plans can be in the draft or published state. If you haven't submitted your {% data variables.product.prodname_marketplace %} listing for approval, a published listing will function the same way as draft listings until your app is approved and listed on {% data variables.product.prodname_marketplace %}. Draft listings allow you to create and save new pricing plans without making them available on your {% data variables.product.prodname_marketplace %} listing page. Once you publish the pricing plan, it's available for customers to purchase immediately. You can publish up to 10 pricing plans.
To create a pricing plan for your {% data variables.product.prodname_marketplace %} listing, click **Plans and pricing** in the left sidebar of your [{% data variables.product.prodname_marketplace %} listing page](https://github.com/marketplace/manage). If you haven't created a {% data variables.product.prodname_marketplace %} listing yet, read "[Creating a draft {% data variables.product.prodname_marketplace %} listing](/marketplace/listing-on-github-marketplace/creating-a-draft-github-marketplace-listing/)" to learn how.
To create a pricing plan for your {% data variables.product.prodname_marketplace %} listing, click **Plans and pricing** in the left sidebar of your [{% data variables.product.prodname_marketplace %} listing page](https://github.com/marketplace/manage). For more information, see "[Creating a draft {% data variables.product.prodname_marketplace %} listing](/marketplace/listing-on-github-marketplace/creating-a-draft-github-marketplace-listing/)."
When you click **New draft plan**, you'll see a form that allows you to customize your pricing plan. You'll need to configure the following fields to create a pricing plan:
#### Plan name
- **Plan name** - Your pricing plan's name will appear on your {% data variables.product.prodname_marketplace %} app's landing page. You can customize the name of your pricing plan to align with the plan's resources, the size of the company that will use the plan, or anything you'd like.
Your pricing plan's name will appear on your {% data variables.product.prodname_marketplace %} app's landing page. You can customize the name of your pricing plan to align to the plan's resources, the size of the company that will use the plan, or anything you'd like.
- **Pricing models** - There are three types of pricing plan: free, flat-rate, and per-unit. All plans require you to process new purchase and cancellation events from the marketplace API. In addition, for paid plans:
#### Pricing models
- You must set a price for both monthly and yearly subscriptions in US dollars.
- Your app must process plan change events.
- You must request verification to publish a listing with a paid plan.
- {% data reusables.marketplace.marketplace-pricing-free-trials %}
##### Free plans
For detailed information, see "[Pricing plans for {% data variables.product.prodname_marketplace %} apps](/developers/github-marketplace/pricing-plans-for-github-marketplace-apps)" and "[Using the {% data variables.product.prodname_marketplace %} API in your app](/developers/github-marketplace/using-the-github-marketplace-api-in-your-app)."
{% data reusables.marketplace.free-apps-encouraged %} A free plan still requires you to handle [new purchase](/marketplace/integrating-with-the-github-marketplace-api/handling-new-purchases-and-free-trials/) and [cancellation](/marketplace/integrating-with-the-github-marketplace-api/cancelling-plans/) billing flows. See "[Billing flows](/marketplace/integrating-with-the-github-marketplace-api/#billing-flows)" for more details.
- **Available for** - {% data variables.product.prodname_marketplace %} pricing plans can apply to **Personal and organization accounts**, **Personal accounts only**, or **Organization accounts only**. For example, if your pricing plan is per-unit and provides multiple seats, you would select **Organization accounts only** because there is no way to assign seats to people in an organization from a personal account.
##### Flat-rate plans
- **Short description** - Write a brief summary of the details of the pricing plan. The description might include the type of customer the plan is intended for or the resources the plan includes.
Flat-rate pricing plans allow you to offer your service to customers for a flat-rate fee. {% data reusables.marketplace.marketplace-pricing-free-trials %}
- **Bullets** - You can write up to four bullets that include more details about your pricing plan. The bullets might include the use cases of your app or list more detailed information about the resources or features included in the plan.
You must set a price for both monthly and yearly subscriptions in U.S. Dollars for flat-rate plans.
##### Per-unit plans
Per-unit pricing allows you to offer your app in units. For example, a unit can be a person, seat, or user. You'll need to provide a name for the unit and set a price for both monthly and yearly subscriptions, in U.S. Dollars.
#### Available for
{% data variables.product.prodname_marketplace %} pricing plans can apply to **Personal and organization accounts**, **Personal accounts only**, or **Organization accounts only**. For example, if your pricing plan is per-unit and provides multiple seats, you would select **Organization accounts only** because there is no way to assign seats to people in an organization from a personal account.
#### Short description
Write a brief summary of the details of the pricing plan. The description might include the type of customer the plan is intended for or the resources the plan includes.
#### Bullets
You can write up to four bullets that include more details about your pricing plan. The bullets might include the use cases of your app or list more detailed information about the resources or features included in the plan.
{% data reusables.marketplace.free-plan-note %}
### Changing a {% data variables.product.prodname_marketplace %} listing's pricing plan
If a pricing plan for your {% data variables.product.prodname_marketplace %} plan is no longer needed or if you need to adjust pricing details, you can remove it.
If a pricing plan for your {% data variables.product.prodname_marketplace %} listing is no longer needed, or if you need to adjust pricing details, you can remove it.
![Button to remove your pricing plan](/assets/images/marketplace/marketplace_remove_this_plan.png)
Once you publish a pricing plan for an app already listed in the {% data variables.product.prodname_marketplace %}, you can't make changes to the plan. Instead, you'll need to remove the pricing plan. Customers who already purchased the removed pricing plan will continue to use it until they opt out and move onto a new pricing plan. For more on pricing plans, see "[{% data variables.product.prodname_marketplace %} pricing plans](/marketplace/selling-your-app/github-marketplace-pricing-plans/)."
Once you publish a pricing plan for an app that is already listed in {% data variables.product.prodname_marketplace %}, you can't make changes to the plan. Instead, you'll need to remove the pricing plan and create a new plan. Customers who already purchased the removed pricing plan will continue to use it until they opt out and move onto a new pricing plan. For more on pricing plans, see "[{% data variables.product.prodname_marketplace %} pricing plans](/marketplace/selling-your-app/github-marketplace-pricing-plans/)."
Once you remove a pricing plan, users won't be able to purchase your app using that plan. Existing users on the removed pricing plan will continue to stay on the plan until they cancel their plan subscription.

Просмотреть файл

@ -0,0 +1,37 @@
---
title: Submitting your listing for publication
intro: 'You can submit your listing for the {% data variables.product.prodname_dotcom %} community to use.'
redirect_from:
- /marketplace/listing-on-github-marketplace/submitting-your-listing-for-review
- /developers/github-marketplace/submitting-your-listing-for-review
versions:
free-pro-team: '*'
---
Once you've completed the listing for your app, you'll see two buttons that allow you to request publication of the listing with or without verification. The **Request** button for "Publish without verification" is disabled if you have published any paid pricing plans in the listing.
![Unverified and verified request button](/assets/images/marketplace/marketplace-request-button.png)
{% data reusables.marketplace.launch-with-free %}
After you submit your listing for review, an onboarding expert will reach out to you with additional information.
For an overview of the process for creating and submitting a listing, see "[About {% data variables.product.prodname_marketplace %}](/developers/github-marketplace/about-github-marketplace#publishing-an-app-to-github-marketplace)."
### Prerequisites for publishing with verification
Before you request verification of your listing, you'll need to integrate the {% data variables.product.prodname_marketplace %} billing flows and webhook into your app. For more information, see "[Using the {% data variables.product.prodname_marketplace %} API in your app](/developers/github-marketplace/using-the-github-marketplace-api-in-your-app)."
If you've met the requirements for listing and you've integrated with the {% data variables.product.prodname_marketplace %} API, go ahead and submit your listing. For more information, see "[Requirements for listing an app](/developers/github-marketplace/requirements-for-listing-an-app)."
{% data reusables.marketplace.app-transfer-to-org-for-verification %} For information on how to do this, see: "[Transferring an app to an organization before you submit](#transferring-an-app-to-an-organization-before-you-submit)" below.
### Transferring an app to an organization before you submit
You cannot sell an app that's owned by a user account. You need to transfer the app to an organization that is already a verified creator, or that can request verification for a listing for the app. For details, see:
1. "[Creating an organization from scratch](/github/setting-up-and-managing-organizations-and-teams/creating-a-new-organization-from-scratch)"
1. "[Transferring ownership of a GitHub App](/developers/apps/transferring-ownership-of-a-github-app)" or "[Transferring ownership of an OAuth App](/developers/apps/transferring-ownership-of-an-oauth-app)"

Просмотреть файл

@ -1,22 +0,0 @@
---
title: Submitting your listing for review
intro: 'You can submit your listing as a verified or unverified app for the {% data variables.product.prodname_dotcom %} community to use.'
redirect_from:
- /marketplace/listing-on-github-marketplace/submitting-your-listing-for-review
versions:
free-pro-team: '*'
---
Once you've completed your app listing, you'll see two buttons that allow you to submit an unverified and verified app. The Publish without Verification **Request** button will not be available if you have published any paid pricing plans.
![Unverified and verified request button](/assets/images/marketplace/marketplace-request-button.png)
{% data reusables.marketplace.launch-with-free %}
Before you can submit a verified app, you'll need to integrate the {% data variables.product.prodname_marketplace %} billing flows and webhook into your existing app. See [Verified apps](/marketplace/#verified-apps) for the steps required to submit your app.
If you've met the [requirements](/marketplace/getting-started/requirements-for-listing-an-app-on-github-marketplace/) for a verified {% data variables.product.prodname_marketplace %} listing and you've integrated with the {% data variables.product.prodname_marketplace %} API, go ahead and submit your listing!
After you submit your listing for review, the {% data variables.product.prodname_marketplace %} onboarding team will reach out to you with additional information.

Просмотреть файл

@ -1,6 +1,6 @@
---
title: Testing your app
intro: 'GitHub recommends testing your app with APIs and webhooks before submitting your listing to {% data variables.product.prodname_marketplace %} so you can provide an ideal experience for customers. Before the {% data variables.product.prodname_marketplace %} onboarding team approves your app, it must adequately handle the [billing flows](/marketplace/integrating-with-the-github-marketplace-api/#billing-flows).'
intro: 'GitHub recommends testing your app with APIs and webhooks before submitting your listing to {% data variables.product.prodname_marketplace %} so you can provide an ideal experience for customers. Before an onboarding expert approves your app, it must adequately handle the billing flows.'
redirect_from:
- /apps/marketplace/testing-apps-apis-and-webhooks/
- /apps/marketplace/integrating-with-the-github-marketplace-api/testing-github-marketplace-apps/
@ -13,7 +13,7 @@ versions:
### Testing apps
You can use a [draft {% data variables.product.prodname_marketplace %} listing](/marketplace/listing-on-github-marketplace/creating-a-draft-github-marketplace-listing/) to simulate each of the [billing flows](/marketplace/integrating-with-the-github-marketplace-api/#billing-flows). A listing in the draft state means that it has not been submitted for approval. Any purchases you make using a draft {% data variables.product.prodname_marketplace %} listing will _not_ create real transactions, and GitHub will not charge your credit card.
You can use a draft {% data variables.product.prodname_marketplace %} listing to simulate each of the billing flows. A listing in the draft state means that it has not been submitted for approval. Any purchases you make using a draft {% data variables.product.prodname_marketplace %} listing will _not_ create real transactions, and GitHub will not charge your credit card. For more information, see "[Drafting a listing for your app](/developers/github-marketplace/drafting-a-listing-for-your-app)" and "[Using the {% data variables.product.prodname_marketplace %} API in your app](/developers/github-marketplace/using-the-github-marketplace-api-in-your-app)."
#### Using a development app with a draft listing to test changes

Просмотреть файл

@ -1,6 +1,6 @@
---
title: Webhook events for the GitHub Marketplace API
intro: 'A {% data variables.product.prodname_marketplace %} app receives information about changes to a user''s plan from the Marketplace purchase event webhook. A Marketplace purchase event is triggered when a user purchases, cancels, or changes their payment plan. For details on how to respond to each of these types of events, see "[Billing flows](/marketplace/integrating-with-the-github-marketplace-api/#billing-flows)."'
intro: 'A {% data variables.product.prodname_marketplace %} app receives information about changes to a user''s plan from the Marketplace purchase event webhook. A Marketplace purchase event is triggered when a user purchases, cancels, or changes their payment plan.'
redirect_from:
- /apps/marketplace/setting-up-github-marketplace-webhooks/about-webhook-payloads-for-a-github-marketplace-listing/
- /apps/marketplace/integrating-with-the-github-marketplace-api/github-marketplace-webhook-events/

Просмотреть файл

@ -445,7 +445,7 @@ Key | Type | Description
#### Webhook payload object
{% data reusables.webhooks.installation_properties %}
{% data reusables.webhooks.app_desc %}
{% data reusables.webhooks.app_always_desc %}
{% data reusables.webhooks.sender_desc %}
#### Webhook payload example
@ -469,7 +469,7 @@ Key | Type | Description
#### Webhook payload object
{% data reusables.webhooks.installation_repositories_properties %}
{% data reusables.webhooks.app_desc %}
{% data reusables.webhooks.app_always_desc %}
{% data reusables.webhooks.sender_desc %}
#### Webhook payload example

Просмотреть файл

@ -21,7 +21,9 @@ Repository owners and people with write access can enable {% data variables.prod
When you first enable a {% data variables.product.prodname_discussions %}, you will be invited to configure a welcome post.
{% data reusables.repositories.navigate-to-repo %}
{% data reusables.repositories.sidebar-settings %}
1. Under your repository name, click {% octicon "gear" aria-label="The gear icon" %}
**Settings**.
![Public settings button](/assets/images/help/discussions/public-repo-settings.png)
1. Under "Features", click **Set up discussions**.
![Set up a discussion button under "Features" for enabling or disabling discussions for a repository](/assets/images/help/discussions/setup-discussions-button.png)
1. Under "Start a new discussion," edit the template to align with the resources and tone you want to set for your community.

Просмотреть файл

@ -4,6 +4,7 @@ intro: You can create an assignment for students in your course to complete indi
versions:
free-pro-team: '*'
redirect_from:
- /education/manage-coursework-with-github-classroom/creating-an-individual-assignment
- /education/manage-coursework-with-github-classroom/create-an-individual-assignment
---

Просмотреть файл

@ -7,26 +7,27 @@ versions:
free-pro-team: '*'
---
You can discover, browse, and install free and paid tools, including [{% data variables.product.prodname_github_app %}s, {% data variables.product.prodname_oauth_app %}s](/apps/differences-between-apps/), and {% data variables.product.prodname_actions %}, in [{% data variables.product.prodname_marketplace %}](https://github.com/marketplace).
You can discover, browse, and install free and paid tools, including {% data variables.product.prodname_github_app %}s, {% data variables.product.prodname_oauth_app %}s, and {% data variables.product.prodname_actions %}, in [{% data variables.product.prodname_marketplace %}](https://github.com/marketplace).
If you purchase a paid tool, you'll pay for your tool subscription with the same billing information you use to pay for your {% data variables.product.product_name %} subscription, and receive one bill on your regular billing date. For more information, see "[About billing for {% data variables.product.prodname_marketplace %}](/articles/about-billing-for-github-marketplace)."
You may also have the option to select a free 14-day trial on select tools. You can cancel at any time during your trial and you won't be charged, but you will automatically lose access to the tool. Your paid subscription will start at the end of the 14-day trial. For more information, see "[About billing for {% data variables.product.prodname_marketplace %}](/articles/about-billing-for-github-marketplace)."
You may also have the option to select a free 14-day trial on some tools. You can cancel at any time during your trial and you won't be charged, but you will automatically lose access to the tool. Your paid subscription will start at the end of the 14-day trial. For more information, see "[About billing for {% data variables.product.prodname_marketplace %}](/articles/about-billing-for-github-marketplace)."
{% data variables.product.prodname_github_app %}s and {% data variables.product.prodname_oauth_app %}s can be verified or unverified. Verified apps meet specific requirements set by {% data variables.product.prodname_dotcom %} and go through a security review before they are listed on {% data variables.product.prodname_marketplace %}. For more information, see "[Requirements for listing an app on GitHub Marketplace](/marketplace/getting-started/requirements-for-listing-an-app-on-github-marketplace/)."
### Finding tools on {% data variables.product.prodname_marketplace %}
### {% data variables.product.prodname_actions %} on {% data variables.product.prodname_marketplace %}
You can discover, browse, and install apps and actions created by others on {% data variables.product.prodname_marketplace %}, see "[Searching {% data variables.product.prodname_marketplace %}](/github/searching-for-information-on-github/searching-github-marketplace)."
You can discover, browse, and install {% data variables.product.prodname_actions %} created by others on {% data variables.product.prodname_marketplace %}, see "[Searching {% data variables.product.prodname_marketplace %}](/github/searching-for-information-on-github/searching-github-marketplace)."
{% data reusables.actions.actions-not-verified %}
Anyone can list an action on {% data variables.product.prodname_marketplace %}. Unlike some apps, {% data variables.product.prodname_actions %} listed on {% data variables.product.prodname_marketplace %} are never verified by {% data variables.product.prodname_dotcom %}.
Anyone can list a free {% data variables.product.prodname_github_app %} or {% data variables.product.prodname_oauth_app %} on {% data variables.product.prodname_marketplace %}. Publishers of paid apps are verified by {% data variables.product.company_short %} and listings for these apps are shown with a verified creator badge {% octicon "verified" aria-label="Verified creator badge" %}. You will also see badges for unverified and verified apps. These apps were published using the previous method for verifying individual apps. For more information about the current process, see "[About verified creators](/developers/github-marketplace/about-verified-creators)" and "[Requirements for listing an app](/developers/github-marketplace/requirements-for-listing-an-app)."
### Building and listing a tool on {% data variables.product.prodname_marketplace %}
For more information on creating your own tool to list on {% data variables.product.prodname_marketplace %}, see "[Apps](/apps)" and "[{% data variables.product.prodname_actions %}](/actions)."
For more information on creating your own tool to list on {% data variables.product.prodname_marketplace %}, see "[Apps](/developers/apps)" and "[{% data variables.product.prodname_actions %}](/actions)."
### Further reading
- "[Purchasing and installing apps in {% data variables.product.prodname_marketplace %}](/articles/purchasing-and-installing-apps-in-github-marketplace)"
- "[Managing billing for {% data variables.product.prodname_marketplace %} apps](/articles/managing-billing-for-github-marketplace-apps)"
- "[{% data variables.product.prodname_marketplace %} support](/articles/github-marketplace-support)"
- "[Differences between GitHub Apps and OAuth Apps](/developers/apps/differences-between-github-apps-and-oauth-apps)"

Просмотреть файл

@ -10,8 +10,8 @@ versions:
---
{% for term in site.data.glossaries.external %}
### {{term.term}}
{{term.description}}
### {% data glossaries.external[forloop.index0].term %}
{% data glossaries.external[forloop.index0].description %}
---
{% endfor %}

Просмотреть файл

@ -2,6 +2,7 @@
title: Working with GitHub Support
redirect_from:
- /categories/working-with-github-support
- /forum
versions:
free-pro-team: '*'
---

10
content/graphql/README.md Normal file
Просмотреть файл

@ -0,0 +1,10 @@
# GraphQL
The `/content/graphql` directory is where the GitHub GraphQL API docs live!
* The `/content/graphql/guides` and `/content/graphql/overview` directories contain articles that are human-editable.
* The `/content/graphql/reference` directory contains an article for each GraphQL data type used in the GitHub GraphQL API. Most of the content in this directory is rendered using `include` tags.
The content rendered by `include` tags is sourced from the `/lib/graphql/static` directory, which is automatically generated from the API source code internally in GitHub, and should not be edited by a human. For more information, see the [`/lib/graphql/README.md`](/lib/graphql/README.md).
**As a result, we cannot accept contributions to GraphQL API reference content in this repository.**

10
content/rest/README.md Normal file
Просмотреть файл

@ -0,0 +1,10 @@
# REST
The `/content/rest` directory is where the GitHub REST API docs live!
* The `/content/rest/guides` and `/content/rest/overview` directories contain regular articles. These are human-editable.
* The `/content/rest/reference` directory contains an article for each group of endpoints in the GitHub REST API. Most of the content in this directory is rendered using `include` tags.
The content rendered by `include` tags is sourced from the `/lib/rest/static` directory, which is automatically generated from the API source code internally in GitHub, and should not be edited by a human. For more information, see the [`/lib/rest/README.md`](/lib/rest/README.md).
**As a result, we cannot accept contributions to REST API reference content in this repository.**

Просмотреть файл

@ -63,7 +63,7 @@ curl -H 'Authorization: token my-oauth-token' https://api.github.com/user/repos
#### Calls to OAuth Authorizations API
If you're making [OAuth Authorization API](/enterprise-server@2.22/rest/reference/oauth-authorizations) calls to manage your OAuth app's authorizations or to generate access tokens, similar to this example:
If you're making [OAuth Authorization API](/enterprise-server/rest/reference/oauth-authorizations) calls to manage your OAuth app's authorizations or to generate access tokens, similar to this example:
```bash
curl -u my_username:my_password -X POST "https://api.github.com/authorizations" -d '{"scopes":["public_repo"], "note":"my token", "client_id":"my_client_id", "client_secret":"my_client_secret"}'

Просмотреть файл

@ -1 +1 @@
Anyone can publish an action in {% data variables.product.prodname_marketplace %}. {% data variables.product.prodname_dotcom %} verifies some partner organizations, but unlike verified apps, {% data variables.product.prodname_dotcom %} does not review or verify individual actions listed in {% data variables.product.prodname_marketplace %}.
Anyone can publish an action in {% data variables.product.prodname_marketplace %}. {% data variables.product.prodname_dotcom %} verifies some partner organizations and these are shown as verified creators.

Просмотреть файл

@ -1,6 +1,8 @@
You can enable or disable discussions for a repository.
{% data reusables.repositories.navigate-to-repo %}
{% data reusables.repositories.sidebar-settings %}
1. Under your repository name, click {% octicon "gear" aria-label="The gear icon" %}
**Settings**.
![Repository settings button](/assets/images/help/discussions/public-repo-settings.png)
1. Under "Features", select **Discussions**.
![Checkbox under "Features" for enabling or disabling discussions for a repository](/assets/images/help/discussions/select-discussions-checkbox.png)

Просмотреть файл

@ -0,0 +1 @@
If you want to sell an app that's owned by your user account, first you'll need to transfer the app to an organization, and then request verification for a listing created by the organization.

Просмотреть файл

@ -0,0 +1,5 @@
{% note %}
**Note:** If you're listing an app on {% data variables.product.prodname_marketplace %}, you can't list your app with a free pricing plan if you offer a paid service outside of {% data variables.product.prodname_marketplace %}.
{% endnote %}

Просмотреть файл

@ -1 +1 @@
You can submit both an unverified and verified app. This will allow you to launch with a free version of your app. Once GitHub verifies your app, your listing will change from unverified to verified in {% data variables.product.prodname_marketplace %} and GitHub will publish your new pricing plans.
You can request publication with or without verification. Requesting publication without verification allows you to launch a free version of your app quickly. If you then request publication with verification, your listing will be updated to include the verified creator badge and any paid pricing plans when you complete verification and financial onboarding.

Просмотреть файл

@ -1,7 +1,7 @@
- Customers who cancel a paid plan purchased from {% data variables.product.prodname_marketplace %} must be automatically downgraded to the app's free plan if it exists. {% data reusables.marketplace.cancellation-clarification %} It's highly recommended to allow customers to re-enable their previous plan.
- Customers must be able to upgrade from your app's UI if you provide an [upgrade URL](/marketplace/integrating-with-the-github-marketplace-api/upgrading-and-downgrading-plans/#about-upgrade-urls) in this format: `https://www.github.com/marketplace/<LISTING_NAME>/upgrade/<LISTING_PLAN_NUMBER>/<CUSTOMER_ACCOUNT_ID>`
- Customers must be able to modify which users have access to your app from your app's website if they purchased seats (per-unit pricing plan) or the plan offers unlimited collaborators.
- Customers must be able to see the following changes to their account immediately in the billing, profile, or account settings section of the app's website:
- Customers who cancel a paid plan purchased from {% data variables.product.prodname_marketplace %} should be automatically downgraded to the app's free plan if it exists. {% data reusables.marketplace.cancellation-clarification %} It's highly recommended to allow customers to re-enable their previous plan.
- Customers should be able to upgrade from your app's user interface if you provide an [upgrade URL](/marketplace/integrating-with-the-github-marketplace-api/upgrading-and-downgrading-plans/#about-upgrade-urls) in this format: `https://www.github.com/marketplace/<LISTING_NAME>/upgrade/<LISTING_PLAN_NUMBER>/<CUSTOMER_ACCOUNT_ID>`
- Customers should be able to modify which users have access to your app from your app's website if they purchased seats (per-unit pricing plan) or the plan offers unlimited collaborators.
- Customers should be able to see the following changes to their account immediately in the billing, profile, or account settings section of the app's website:
- Current plan and price.
- New plans purchased.
- Upgrades, downgrades, cancellations, and the number of remaining days in a free trial.

Просмотреть файл

@ -0,0 +1 @@
`installation` | `object` | The {% data variables.product.prodname_github_app %} installation.

Просмотреть файл

@ -17,7 +17,7 @@ header:
still in translation. For the most up-to-date and accurate information,
please visit our
<a id="to-english-doc" href="/en">English documentation</a>.
early_access: 👋 This page contains content about an early access feature. Please do not share this URL publicly.
early_access: 📣 Please <b>do not share</b> this URL publicly. This page contains content about an early access feature.
search:
need_help: Need help?
placeholder: Search topics, products...
@ -66,6 +66,14 @@ contribution_cta:
button: Make a contribution
or: Or,
to_guidelines: learn how to contribute.
enterprise_releases_list:
title: Enterprise Server Releases
currently_supported: Currently supported
currently_supported_message: See <a href="https://github.com/enterprise">GitHub Enterprise</a> for information about the latest release.
deprecated: Deprecated
deprecated_message: 'These docs remain available but are no longer maintained:'
deprecated_developer: Deprecated on developer.github.com
deprecated_developer_message: 'These docs remain available on the legacy <a href="https://developer.github.com">developer site</a> but are no longer maintained:'
products:
graphql:
reference:

Просмотреть файл

@ -1,10 +1,3 @@
- title: Starter workflows
description: Workflow files for helping people get started with GitHub Actions
languages: TypeScript
href: actions/starter-workflows
tags:
- official
- workflow
- title: Example services
description: Example workflows using service containers
languages: JavaScript

Просмотреть файл

@ -1,5 +1,5 @@
{% if page.permalinks and page.permalinks.length > 1 %}
<details class="dropdown-withArrow d-inline-block details details-reset mb-4 position-relative close-when-clicked-outside article-versions">
<details class="dropdown-withArrow d-inline-block details details-reset mb-1 position-relative close-when-clicked-outside article-versions">
<summary class="d-flex flex-items-center flex-justify-between f4 h5-mktg btn-outline-mktg btn-mktg p-2">
<!-- GitHub.com, Enterprise Server 2.16, etc -->
<span class="d-md-none d-xl-inline-block mr-1">{% data ui.pages.article_version %}</span> {{ allVersions[currentVersion].versionTitle }}
@ -14,6 +14,7 @@
>
{{ allVersions[permalink.pageVersion].versionTitle }}</a>
{% endfor %}
<a class="f6 no-underline text-gray-light pt-1" href="/enterprise-server-releases">See all Enterprise releases</a>
</div>
</details>
{% endif %}

Просмотреть файл

@ -1,7 +1,7 @@
<nav class="breadcrumbs f5" aria-label="Breadcrumb">
{% for breadcrumb in breadcrumbs %}
{% if page.hidden %}
<span class="d-inline-block">{{breadcrumb[1].title}}</span>
{% 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 %}">
{{breadcrumb[1].title}}</a>

Просмотреть файл

@ -1,6 +1,6 @@
<head>
<meta charset="utf-8" />
<title>{% if error == '404' %}{% data ui.errors.oops %}{% elsif currentVersion == 'homepage' %}GitHub Documentation{% else %}{{ page.fullTitle }}{% endif %}</title>
<title>{% if error == '404' %}{% data ui.errors.oops %}{% elsif currentVersion == 'homepage' %}GitHub Documentation{% elsif page.fullTitle %}{{ page.fullTitle }}{% else %}GitHub Documentation{% endif %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">{% if page.hidden %}
<meta name="robots" content="noindex" />{% endif %}
<meta name="google-site-verification" content="OgdQc0GZfjDI52wDv1bkMT-SLpBUo_h5nn9mI9L22xQ" />

Просмотреть файл

@ -49,7 +49,7 @@
{% endif %}
{% if early_access_notification_type %}
<div class="header-notifications text-center f5 bg-blue-1 text-gray-dark py-4 px-6 early_access">
<div class="header-notifications text-center f5 bg-red-1 text-gray-dark py-4 px-6 early_access">
{{ early_access_notification }}
</div>
{% endif %}

Просмотреть файл

@ -0,0 +1,64 @@
<!doctype html>
<html lang="{{currentLanguage}}">
{% include head %}
<body class="d-lg-flex">
{% include sidebar %}
<main class="width-full">
<main class="container-xl px-3 px-md-6 my-4 my-lg-4 d-xl-flex">
<article class="markdown-body width-full">
<div class="d-lg-flex flex-justify-between"></div>
<div class="mt-2 article-grid-container">
<div class="article-grid-head">
<div class="d-flex flex-items-baseline flex-justify-between mt-3">
<h1 class="border-bottom-0">{% data ui.enterprise_releases_list.title %}</h1>
</div>
</div>
<div class="article-grid-toc border-bottom border-xl-0 pb-4 mb-5 pb-xl-0 mb-xl-0">
<div class="article-grid-toc-content">
{% if miniTocItems.size > 1 %}
<h3 id="in-this-article" class="f5 mb-2"><a class="link-gray-dark" href="#in-this-article">{% data ui.pages.miniToc %}</a></h3>
<ul class="list-style-none pl-0 f5 mb-0">
{% for item in miniTocItems %}
<li class="ml-{{ item.indentationLevel | times: 3 }} mb-2 lh-condensed">{{ item.contents }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
<div id="article-contents" class="article-grid-body">
<h2 id="currently-supported"><a href="#currently-supported">{% data ui.enterprise_releases_list.currently_supported %}</a></h2>
<p>{% data ui.enterprise_releases_list.currently_supported_message %}</p>
<ul>
{% for version in enterpriseServerReleases.supported %}
<li><a href="/{{currentLanguage}}/enterprise-server@{{version}}">Enterprise Server {{version}}</a></li>
{% endfor %}
</ul>
<h2 id="deprecated"><a href="#deprecated">{% data ui.enterprise_releases_list.deprecated %}</a></h2>
<p>{% data ui.enterprise_releases_list.deprecated_message %}</p>
<ul>
{% for version in enterpriseServerReleases.deprecatedReleasesWithNewFormat %}
<li><a href="/{{currentLanguage}}/enterprise-server@{{version}}">Enterprise Server {{version}}</a></li>
{% endfor %}
{% for version in enterpriseServerReleases.deprecatedReleasesWithLegacyFormat %}
<li><a href="/{{currentLanguage}}/enterprise/{{version}}">Enterprise Server {{version}}</a></li>
{% endfor %}
</ul>
<h2 id="deprecated-on-developer.github.com"><a href="#deprecated-on-developer.github.com">{% data ui.enterprise_releases_list.deprecated_developer %}</a></h2>
<p>{% data ui.enterprise_releases_list.deprecated_developer_message %}</p>
{% for version in enterpriseServerReleases.deprecatedReleasesOnDeveloperSite %}
<li><a href="https://developer.github.com/enterprise/{{version}}">Enterprise Server {{version}}</a></li>
{% endfor %}
{% include support %}
{% include small-footer %}
</div>
</div>
</article>
</main>
</main>
</body>
</html>

68
lib/data-directory.js Normal file
Просмотреть файл

@ -0,0 +1,68 @@
const assert = require('assert')
const fs = require('fs').promises
const path = require('path')
const walk = require('walk-sync')
const yaml = require('js-yaml')
const { isRegExp, set } = require('lodash')
const filenameToKey = require('./filename-to-key')
module.exports = async function dataDirectory (dir, opts = {}) {
const defaultOpts = {
preprocess: (content) => { return content },
ignorePatterns: [/README\.md$/i],
extensions: [
'.json',
'.md',
'.markdown',
'.yaml',
'.yml'
]
}
opts = Object.assign({}, defaultOpts, opts)
// validate input
assert(Array.isArray(opts.ignorePatterns))
assert(opts.ignorePatterns.every(isRegExp))
assert(Array.isArray(opts.extensions))
assert(opts.extensions.length)
// start with an empty data object
const data = {}
// find YAML and Markdown files in the given directory, recursively
await Promise.all(walk(dir, { includeBasePath: true })
.filter(filename => {
// ignore files that match any of ignorePatterns regexes
if (opts.ignorePatterns.some(pattern => pattern.test(filename))) return false
// ignore files that don't have a whitelisted file extension
return opts.extensions.includes(path.extname(filename).toLowerCase())
})
.map(async filename => {
// derive `foo.bar.baz` object key from `foo/bar/baz.yml` filename
const key = filenameToKey(path.relative(dir, filename))
const extension = path.extname(filename).toLowerCase()
let fileContent = await fs.readFile(filename, 'utf8')
if (opts.preprocess) fileContent = opts.preprocess(fileContent)
// add this file's data to the global data object
switch (extension) {
case '.json':
set(data, key, JSON.parse(fileContent))
break
case '.yaml':
case '.yml':
set(data, key, yaml.safeLoad(fileContent, { filename }))
break
case '.md':
case '.markdown':
set(data, key, fileContent)
break
}
}))
return data
}

Просмотреть файл

@ -44,6 +44,9 @@ const lastVersionWithoutStubbedRedirectFiles = '2.17'
// last version using paths like /enterprise/<release>/<user>/<product>/<category>/<article>
// instead of /enterprise-server@<release>/<product>/<category>/<article>
const lastReleaseWithLegacyFormat = '2.18'
const deprecatedReleasesWithLegacyFormat = deprecated.filter(version => versionSatisfiesRange(version, '<=2.18'))
const deprecatedReleasesWithNewFormat = deprecated.filter(version => versionSatisfiesRange(version, '>2.18'))
const deprecatedReleasesOnDeveloperSite = deprecated.filter(version => versionSatisfiesRange(version, '<=2.16'))
module.exports = {
supported,
@ -57,5 +60,8 @@ module.exports = {
dates,
firstVersionDeprecatedOnNewSite,
lastVersionWithoutStubbedRedirectFiles,
lastReleaseWithLegacyFormat
lastReleaseWithLegacyFormat,
deprecatedReleasesWithLegacyFormat,
deprecatedReleasesWithNewFormat,
deprecatedReleasesOnDeveloperSite
}

28
lib/filename-to-key.js Normal file
Просмотреть файл

@ -0,0 +1,28 @@
/* eslint-disable prefer-regex-literals */
const path = require('path')
const { escapeRegExp } = require('lodash')
// slash at the beginning of a filename
const leadingPathSeparator = new RegExp(`^${escapeRegExp(path.sep)}`)
const windowsLeadingPathSeparator = new RegExp('^/')
// all slashes in the filename. path.sep is OS agnostic (windows, mac, etc)
const pathSeparator = new RegExp(escapeRegExp(path.sep), 'g')
const windowsPathSeparator = new RegExp('/', 'g')
// handle MS Windows style double-backslashed filenames
const windowsDoubleSlashSeparator = new RegExp('\\\\', 'g')
// derive `foo.bar.baz` object key from `foo/bar/baz.yml` filename
module.exports = function filenameToKey (filename) {
const extension = new RegExp(`${path.extname(filename)}$`)
const key = filename
.replace(extension, '')
.replace(leadingPathSeparator, '')
.replace(windowsLeadingPathSeparator, '')
.replace(pathSeparator, '.')
.replace(windowsPathSeparator, '.')
.replace(windowsDoubleSlashSeparator, '.')
return key
}

Просмотреть файл

@ -1,6 +1,6 @@
const Liquid = require('liquid')
const Syntax = /([a-z0-9/\\_.-]+)/i
const Syntax = /([a-z0-9/\\_.\-[\]]+)/i
const SyntaxHelp = "Syntax Error in 'data' - Valid syntax: data [path]"
module.exports = class Data extends Liquid.Tag {

Просмотреть файл

@ -1,5 +1,5 @@
const assert = require('assert')
const fs = require('fs')
const fs = require('fs').promises
const path = require('path')
const cheerio = require('cheerio')
const patterns = require('./patterns')
@ -23,15 +23,30 @@ const slash = require('slash')
const statsd = require('./statsd')
class Page {
constructor (opts) {
static async init (opts) {
assert(opts.relativePath, 'relativePath is required')
assert(opts.basePath, 'basePath is required')
const relativePath = slash(opts.relativePath)
const fullPath = slash(path.join(opts.basePath, relativePath))
const raw = await fs.readFile(fullPath, 'utf8')
return new Page({ ...opts, relativePath, fullPath, raw })
}
static async exists (path) {
try {
return await fs.stat(path)
} catch (err) {
if (err.code === 'ENOENT') return false
console.error(err)
}
}
constructor (opts) {
assert(opts.languageCode, 'languageCode is required')
Object.assign(this, { ...opts })
this.relativePath = slash(this.relativePath)
this.fullPath = slash(path.join(this.basePath, this.relativePath))
this.raw = fs.readFileSync(this.fullPath, 'utf8')
// TODO remove this when crowdin-support issue 66 has been resolved
if (this.languageCode !== 'en' && this.raw.includes(': verdadero')) {

Просмотреть файл

@ -2,42 +2,50 @@ const path = require('path')
const walk = require('walk-sync').entries
const Page = require('./page')
const languages = require('./languages')
const fs = require('fs')
const { mapLimit, filterLimit } = require('async')
const FILE_READ_LIMIT = 500
async function loadPageList () {
const pageList = []
// load english pages
const englishPath = path.join(__dirname, '..', languages.en.dir, 'content')
const englishPages = walk(englishPath)
.filter(({ relativePath }) => {
return relativePath.endsWith('.md') &&
!relativePath.includes('README')
})
.map(fileData => new Page({ ...fileData, languageCode: languages.en.code }))
const englishPaths = walk(englishPath)
.filter(({ relativePath }) =>
relativePath.endsWith('.md') && !relativePath.includes('README')
)
const englishPages = await mapLimit(
englishPaths,
FILE_READ_LIMIT,
async fileData => await Page.init({ ...fileData, languageCode: languages.en.code })
)
pageList.push(...englishPages)
// load matching pages in other languages
for (const page of englishPages) {
for (const language of Object.values(languages)) {
if (language.code === 'en') continue
let localizedPaths = Object.values(languages)
.filter(({ code }) => code !== 'en')
.map(language => {
const basePath = path.join(__dirname, '..', language.dir, 'content')
const localizedPath = path.join(basePath, page.relativePath)
try {
fs.statSync(localizedPath)
} catch (_) {
continue
}
pageList.push(new Page({
relativePath: page.relativePath,
return englishPages.map(page => ({
basePath,
relativePath: page.relativePath,
localizedPath: path.join(basePath, page.relativePath),
languageCode: language.code
}))
}
}
})
.flat()
localizedPaths = await filterLimit(
localizedPaths,
FILE_READ_LIMIT,
async ({ localizedPath }) => Page.exists(localizedPath)
)
const localizedPages = await mapLimit(
localizedPaths,
FILE_READ_LIMIT,
async ({ basePath, relativePath, languageCode }) =>
await Page.init({ basePath, relativePath, languageCode })
)
pageList.push(...localizedPages)
return pageList
}

Просмотреть файл

@ -15,7 +15,25 @@ module.exports = function getDocsPathFromDeveloperPath (oldDeveloperPath, allRed
// oneoff redirect
const v3OrgPreReceiveHooks = '/v3/orgs/pre_receive_hooks'
if (newPath.endsWith(v3OrgPreReceiveHooks)) {
newPath = newPath.replace(v3OrgPreReceiveHooks, '/v3/enterprise-admin/org_pre_receive_hooks')
newPath = newPath.replace(v3OrgPreReceiveHooks, '/v3/enterprise-admin/organization_pre_receive_hooks')
}
// oneoff redirect
const v3RepoPreReceiveHooks = '/v3/repos/pre_receive_hooks'
if (newPath.endsWith(v3RepoPreReceiveHooks)) {
newPath = newPath.replace(v3RepoPreReceiveHooks, '/v3/enterprise-admin/repository_pre_receive_hooks')
}
// oneoff redirect
const v3OrgHooks = '/v3/orgs/hooks'
if (newPath.endsWith(v3OrgHooks)) {
newPath = newPath.replace(v3OrgHooks, '/v3/orgs/webhooks')
}
// oneoff redirect
const v3RepoHooks = '/v3/repos/hooks'
if (newPath.endsWith(v3RepoHooks)) {
newPath = newPath.replace(v3RepoHooks, '/v3/repos/webhooks')
}
// oneoff redirect for a dotcom developer path to Enterprise-only path on docs.github.com
@ -46,6 +64,7 @@ module.exports = function getDocsPathFromDeveloperPath (oldDeveloperPath, allRed
.replace(/_/g, '-')
// this is a special oneoff replacement
.replace('org-pre-receive-hooks', 'organization-pre-receive-hooks')
.replace('repo-pre-receive-hooks', 'repository-pre-receive-hooks')
: lastSegment
}

Просмотреть файл

@ -50,22 +50,6 @@ module.exports = async function precompileRedirects (pageList, pageMap) {
const developerRouteWithLanguage = `/en${developerRoute}`
allRedirects[developerRouteWithLanguage] = newPath
// TODO until we update all the old /v3 and /v4 links, we need to support redirects
// from the old /enterprise/<number>/v3 format to the new /enterprise-server@<number/rest format
// AS WELL AS /enterprise-server@<number/v3 to /enterprise-server@<number/rest.
// This is because the new format gets created dynamically even when the links point to /v3 or /v4.
// EXAMPLES:
// /en/enterprise/2.20/v3/pulls/comments -> /en/enterprise-server@2.20/rest/reference/pulls#comments
// /en/enterprise-server@2.20/v3/pulls/comments -> /en/enterprise-server@2.20/rest/reference/pulls#comments
// NOTE: after we update all the /v3 and /v4 links, we can yank the following block
if (developerRoute.includes('/enterprise/')) {
const developerRouteWithNewFormat = developerRoute.replace(/\/enterprise\/(\d.\d\d)\//, '/enterprise-server@$1/')
const developerRouteWithNewFormatWithLanguage = `/en${developerRouteWithNewFormat}`
allRedirects[developerRouteWithNewFormat] = newPath
allRedirects[developerRouteWithNewFormatWithLanguage] = newPath
}
// TODO ENDYANK
// although we only support developer Enterprise paths up to 2.21, we make
// an exception to always redirect versionless paths to the latest version
if (developerRoute.includes('/2.21/')) {
@ -74,33 +58,8 @@ module.exports = async function precompileRedirects (pageList, pageMap) {
const developerRouteWithLanguageWithoutVersion = `/en${developerRouteWithoutVersion}`
allRedirects[developerRouteWithoutVersion] = newPathOnLatestVersion
allRedirects[developerRouteWithLanguageWithoutVersion] = newPathOnLatestVersion
// TODO after we update all the /v3 and /v4 links, we can yank the following
const developerRouteWithoutVersionWithNewFormat = developerRouteWithoutVersion
.replace('/enterprise/', 'enterprise-server')
const developerRouteWithoutVersionWithNewFormatWithLanguage = `/en${developerRouteWithoutVersionWithNewFormat}`
allRedirects[developerRouteWithoutVersionWithNewFormat] = newPathOnLatestVersion
allRedirects[developerRouteWithoutVersionWithNewFormatWithLanguage] = newPathOnLatestVersion
// TODO ENDYANK
}
// TODO: TEMPORARILY support explicit 2.22 redirects (created on page render by lib/rewrite-local-links)
// after we update `/v3` and `/v4` links everywhere to `/rest` and `/graphql`, we can
// yank this entire block because 2.22 never existed on developer site
if (developerRoute.includes('/2.21/')) {
const newPath222 = newPath.replace('@2.21/', '@2.22/')
const developerRoute222 = developerRoute.replace('/2.21/', '/2.22/')
const developerRouteWithLanguage222 = `/en${developerRoute222}`
allRedirects[developerRoute222] = newPath222
allRedirects[developerRouteWithLanguage222] = newPath222
const developerRouteWithNewFormat222 = developerRoute222
.replace('/enterprise/2.22/', '/enterprise-server@2.22/')
const developerRouteWithNewFormatWithLanguage222 = `/en${developerRouteWithNewFormat222}`
allRedirects[developerRouteWithNewFormat222] = newPath222
allRedirects[developerRouteWithNewFormatWithLanguage222] = newPath222
}
// TODO ENDYANK
// given a developer route like `/enterprise/2.19/v3/activity`,
// add a veriation like `/enterprise/2.19/user/v3/activity`;
// we need to do this because all links in content get rewritten

Просмотреть файл

@ -2,6 +2,7 @@ const externalRedirects = Object.keys(require('./redirects/external-sites'))
const pathUtils = require('./path-utils')
const assert = require('assert')
const nonEnterpriseDefaultVersion = require('./non-enterprise-default-version')
const supportedPlans = Object.values(require('./all-versions')).map(v => v.plan)
// Content authors write links like `/some/article/path`, but they need to be
// rewritten on the fly to match the current language and page version
@ -24,11 +25,21 @@ function getNewHref (link, languageCode, version) {
// e.g. `/contact` should not be replaced with `/en/contact`
if (externalRedirects.includes(href)) return
let newHref
// If the link has a hardcoded plan name in it (e.g., /enterprise-server/rest/reference/oauth-authorizations),
// only rewrite it with a language code
if (supportedPlans.includes(href.split('/')[1])) {
newHref = pathUtils.getPathWithLanguage(href, languageCode)
}
// If link is dotcom-only, just get the language code
// Otherwise, get the versioned path with language code
const newHref = link.hasClass('dotcom-only')
if (!newHref) {
newHref = link.hasClass('dotcom-only')
? pathUtils.getVersionedPathWithLanguage(href, nonEnterpriseDefaultVersion, languageCode)
: pathUtils.getVersionedPathWithLanguage(href, version, languageCode)
}
if (href !== newHref) link.attr('href', newHref)
}

Просмотреть файл

@ -2,12 +2,12 @@ const path = require('path')
const flat = require('flat')
const { get, set } = require('lodash')
const languages = require('./languages')
const dataDirectory = require('@github-docs/data-directory')
const dataDirectory = require('./data-directory')
const encodeBracketedParentheticals = require('./encode-bracketed-parentheticals')
const loadSiteDataFromDir = dir => ({
const loadSiteDataFromDir = async dir => ({
site: {
data: dataDirectory(path.join(dir, 'data'), {
data: await dataDirectory(path.join(dir, 'data'), {
preprocess: dataString =>
encodeBracketedParentheticals(dataString.trimEnd()),
ignorePatterns: [/README\.md$/]
@ -18,7 +18,7 @@ const loadSiteDataFromDir = dir => ({
module.exports = async function loadSiteData () {
// load english site data
const siteData = {
en: loadSiteDataFromDir(languages.en.dir)
en: await 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 = loadSiteDataFromDir(language.dir)
const data = await loadSiteDataFromDir(language.dir)
for (const key of englishKeys) {
set(
siteData,

Просмотреть файл

@ -4,6 +4,16 @@ const loadRedirects = require('./redirects/precompile')
const loadSiteData = require('./site-data')
const loadSiteTree = require('./site-tree')
// Instrument these functions so that
// it's wrapped in a timer that reports to Datadog
const dog = {
loadPages: statsd.asyncTimer(loadPages, 'load_pages'),
loadPageMap: statsd.asyncTimer(loadPageMap, 'load_page_map'),
loadRedirects: statsd.asyncTimer(loadRedirects, 'load_redirects'),
loadSiteData: statsd.asyncTimer(loadSiteData, 'load_site_data'),
loadSiteTree: statsd.asyncTimer(loadSiteTree, 'load_site_tree')
}
// For local caching
let pageList, pageMap, site, redirects, siteTree
@ -32,21 +42,21 @@ async function warmServer () {
if (!pageList || !site) {
// Promise.all is used to load multiple things in parallel
[pageList, site] = await Promise.all([
pageList || loadPages(),
site || loadSiteData()
pageList || dog.loadPages(),
site || dog.loadSiteData()
])
}
if (!pageMap) {
pageMap = await loadPageMap(pageList)
pageMap = await dog.loadPageMap(pageList)
}
if (!redirects) {
redirects = await loadRedirects(pageList, pageMap)
redirects = await dog.loadRedirects(pageList, pageMap)
}
if (!siteTree) {
siteTree = await loadSiteTree(pageMap, site, redirects)
siteTree = await dog.loadSiteTree(pageMap, site, redirects)
}
if (process.env.NODE_ENV !== 'test') {
@ -58,7 +68,7 @@ async function warmServer () {
// Instrument the `warmServer` function so that
// it's wrapped in a timer that reports to Datadog
const instrumentedWarmServer = statsd.asyncTimer(warmServer, 'warm_server')
dog.warmServer = statsd.asyncTimer(warmServer, 'warm_server')
// We only want statistics if the priming needs to occur, so let's wrap the
// real method and return early [without statistics] whenever possible
@ -68,5 +78,5 @@ module.exports = async function warmServerWrapper () {
return getWarmedCache()
}
return instrumentedWarmServer()
return dog.warmServer()
}

Просмотреть файл

@ -2,9 +2,12 @@ const path = require('path')
const { getPathWithoutLanguage } = require('../lib/path-utils')
module.exports = async (req, res, next) => {
if (!req.context.page) return next()
if (req.context.page.hidden) return next()
req.context.breadcrumbs = {}
if (!req.context.page) return next()
// Return an empty object on the landing page
if (req.context.page.relativePath === 'index.md') return next()
const rawPath = getPathWithoutLanguage(req.path)

Просмотреть файл

@ -2,7 +2,7 @@
// to quickly search for Help articles by title and insert the link to
// the article into a reply to a customer.
const path = require('path')
const fs = require('fs')
const fs = require('fs').promises
const matter = require('gray-matter')
const dotcomDir = path.join(__dirname, '../content/github')
const dotcomIndex = path.join(dotcomDir, 'index.md')
@ -10,24 +10,22 @@ const linkRegex = /{% (?:topic_)?link_in_list ?\/(.*?) ?%}/g
module.exports = async (req, res, next) => {
if (req.path !== '/categories.json') return next()
const categories = generateCategories()
const categories = await generateCategories()
return res.json(categories)
}
function generateCategories () {
async function generateCategories () {
// get links included in dotcom index page.
// each link corresponds to a dotcom subdirectory
// example: getting-started-with-github
const links = getLinks(fs.readFileSync(dotcomIndex, 'utf8'))
const categories = []
const links = getLinks(await fs.readFile(dotcomIndex, 'utf8'))
// get links included in each subdir's index page
// these are links to articles
links.forEach(link => {
const categories = await Promise.all(links.map(async link => {
const category = {}
const indexPath = getPath(link, 'index')
const indexContents = fs.readFileSync(indexPath, 'utf8')
const indexContents = await fs.readFile(indexPath, 'utf8')
const { data, content } = matter(indexContents)
// get name from title frontmatter
@ -36,29 +34,23 @@ function generateCategories () {
// get child article links
const articleLinks = getLinks(content)
const publishedArticles = []
articleLinks.forEach(articleLink => {
const publishedArticle = {}
category.published_articles = (await Promise.all(articleLinks.map(async articleLink => {
// get title from frontmatter
const articlePath = getPath(link, articleLink)
const articleContents = fs.readFileSync(articlePath, 'utf8')
const articleContents = await fs.readFile(articlePath, 'utf8')
const { data } = matter(articleContents)
// do not include map topics in list of published articles
if (data.mapTopic) return
publishedArticle.title = data.title
publishedArticle.slug = articleLink
return {
title: data.title,
slug: articleLink
}
}))).filter(Boolean)
publishedArticles.push(publishedArticle)
})
category.published_articles = publishedArticles
categories.push(category)
})
return category
}))
return categories
}

Просмотреть файл

@ -1,10 +1,12 @@
const { uniq } = require('lodash')
module.exports = function earlyAccessContext (req, res, next) {
if (process.env.NODE_ENV === 'production') {
return next(404)
}
// Get a list of all hidden pages per version
const earlyAccessPageLinks = req.context.pages
const earlyAccessPageLinks = uniq(Object.values(req.context.pages)
.filter(page => page.hidden)
// Do not include early access landing page
.filter(page => page.relativePath !== 'early-access/index.md')
@ -12,7 +14,7 @@ module.exports = function earlyAccessContext (req, res, next) {
.map(page => {
return page.permalinks.map(permalink => `- [${permalink.title}](${permalink.href})`)
})
.flat()
.flat())
// Get links for the current version
.filter(link => link.includes(req.context.currentVersion))
.sort()

Просмотреть файл

@ -2,8 +2,12 @@
// inline scripts and content from untrusted sources.
const { contentSecurityPolicy } = require('helmet')
const isArchivedVersion = require('../lib/is-archived-version')
const versionSatisfiesRange = require('../lib/version-satisfies-range')
module.exports = contentSecurityPolicy({
// module.exports = contentSecurityPolicy({
module.exports = async (req, res, next) => {
const csp = {
directives: {
defaultSrc: ["'none'"],
connectSrc: [
@ -45,4 +49,19 @@ module.exports = contentSecurityPolicy({
"'self'" // exception for search in deprecated GHE versions
]
}
})
}
const { requestedVersion } = isArchivedVersion(req)
// Exception for Algolia instantsearch in deprecated Enterprise docs (Node.js era)
if (versionSatisfiesRange(requestedVersion, '<=2.19') && versionSatisfiesRange(requestedVersion, '>2.12')) {
csp.directives.scriptSrc.push("'unsafe-eval'")
}
// Exception for search in deprecated Enterprise docs <=2.12 (static site era)
if (versionSatisfiesRange(requestedVersion, '<=2.12')) {
csp.directives.scriptSrc.push("'unsafe-inline'")
}
return contentSecurityPolicy(csp)(req, res, next)
}

Просмотреть файл

@ -0,0 +1,69 @@
const path = require('path')
const { getPathWithoutLanguage } = require('../lib/path-utils')
// Early Access content doesn't conform to the same structure as other products, so we
// can't derive breadcrumbs from the siteTree in the same way. Hence, this separate middleware.
module.exports = async (req, res, next) => {
if (!req.context.page) return next()
if (!req.context.page.hidden) return next()
req.context.breadcrumbs = {}
const earlyAccessProduct = req.context.siteTree[req.language][req.context.currentVersion].products[req.context.currentProduct]
req.context.breadcrumbs.earlyAccessProduct = {
href: '', // no link for the EA product breadcrumb
title: earlyAccessProduct.title
}
const rawPath = getPathWithoutLanguage(req.path)
const pathParts = rawPath.split('/')
// drop first '/'
pathParts.shift()
// drop the version and early-accesss segments so pathParts now starts with /product
pathParts.shift()
pathParts.shift()
// Early Access products do not require an index.md, so look for a matching public product
const product = req.context.siteTree[req.language][req.context.currentVersion].products[pathParts[0]]
if (!product) return next()
req.context.breadcrumbs.product = {
href: '', // no link for the EA product breadcrumbs
title: product.title
}
if (!pathParts[1]) return 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)]
if (!category) return next()
req.context.breadcrumbs.category = {
href: categoryPath, // do include a link for EA category breadcrumbs
title: category.shortTitle || category.title
}
if (!pathParts[2]) return 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)]
if (!mapTopicOrArticle) return next()
const mapTopicOrArticleTitle = await mapTopicOrArticle.renderProp('shortTitle', req.context, { textOnly: true, encodeEntities: true })
req.context.breadcrumbs.article = {
href: mapTopicOrArticlePath, // do include a link for EA map topic/article breadcrumbs
title: mapTopicOrArticleTitle
}
return next()
}

Просмотреть файл

@ -0,0 +1,13 @@
const { liquid } = require('../lib/render-content')
const layouts = require('../lib/layouts')
const getMiniTocItems = require('../lib/get-mini-toc-items')
module.exports = async (req, res, next) => {
if (!req.path.endsWith('/enterprise-server-releases')) return next()
const html = await liquid.parseAndRender(layouts['enterprise-server-releases'], req.context)
req.context.miniTocItems = getMiniTocItems(html)
return res.send(await liquid.parseAndRender(layouts['enterprise-server-releases'], req.context))
}

Просмотреть файл

@ -73,6 +73,8 @@ module.exports = function (app) {
app.use(require('./contextualizers/rest'))
app.use(require('./contextualizers/webhooks'))
app.use(require('./breadcrumbs'))
app.use(require('./early-access-breadcrumbs'))
app.use(require('./enterprise-server-releases'))
app.use(require('./dev-toc'))
app.use(require('./featured-links'))

45
package-lock.json сгенерированный
Просмотреть файл

@ -2969,36 +2969,6 @@
}
}
},
"@github-docs/data-directory": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@github-docs/data-directory/-/data-directory-1.2.0.tgz",
"integrity": "sha512-hp+Ubwl8e77EdnR4OncSUIE7G/cMn9ENOo6ABy8FjqdYCbAWgb/79w7yXVebIV5P3q5r6KAAqPzHj1N5SSrBgw==",
"requires": {
"lodash": "^4.17.15",
"walk-sync": "^2.0.2"
},
"dependencies": {
"matcher-collection": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz",
"integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==",
"requires": {
"@types/minimatch": "^3.0.3",
"minimatch": "^3.0.2"
}
},
"walk-sync": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.1.0.tgz",
"integrity": "sha512-KpH9Xw64LNSx7/UI+3guRZvJWlDxVA4+KKb/4puRoVrG8GkvZRxnF3vhxdjgpoKJGL2TVg1OrtkXIE/VuGPLHQ==",
"requires": {
"@types/minimatch": "^3.0.3",
"ensure-posix-path": "^1.1.0",
"matcher-collection": "^2.0.0"
}
}
}
},
"@github-docs/frontmatter": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@github-docs/frontmatter/-/frontmatter-1.3.1.tgz",
@ -5301,7 +5271,7 @@
},
"agentkeepalive": {
"version": "2.2.0",
"resolved": "http://registry.npmjs.org/agentkeepalive/-/agentkeepalive-2.2.0.tgz",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-2.2.0.tgz",
"integrity": "sha1-xdG9SxKQCPEWPyNvhuX66iAm4u8="
},
"aggregate-error": {
@ -5447,7 +5417,7 @@
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
@ -5577,8 +5547,7 @@
"async": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==",
"dev": true
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
},
"async-each": {
"version": "1.0.3",
@ -6825,7 +6794,7 @@
},
"brfs": {
"version": "1.6.1",
"resolved": "http://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz",
"resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz",
"integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==",
"requires": {
"quote-stream": "^1.0.1",
@ -9434,7 +9403,7 @@
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha1-tKxAZIEH/c3PriQvQovqihTU8b8=",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"requires": {
"is-arrayish": "^0.2.1"
}
@ -12578,7 +12547,7 @@
"dependencies": {
"mkdirp": {
"version": "0.3.0",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
"integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4="
},
"nopt": {
@ -17704,7 +17673,7 @@
},
"magic-string": {
"version": "0.22.5",
"resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
"integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==",
"requires": {
"vlq": "^0.2.2"

Просмотреть файл

@ -19,7 +19,6 @@
"@babel/preset-env": "^7.12.7",
"@babel/preset-react": "^7.12.7",
"@babel/runtime": "^7.11.2",
"@github-docs/data-directory": "^1.2.0",
"@github-docs/frontmatter": "^1.3.1",
"@graphql-inspector/core": "^2.3.0",
"@graphql-tools/load": "^6.2.5",
@ -27,6 +26,7 @@
"@primer/css": "^15.1.0",
"@primer/octicons": "^11.0.0",
"algoliasearch": "^3.35.1",
"async": "^3.2.0",
"babel-loader": "^8.1.0",
"babel-preset-env": "^1.7.0",
"browser-date-formatter": "^3.0.3",
@ -94,7 +94,6 @@
"devDependencies": {
"@actions/core": "^1.2.6",
"ajv": "^6.11.0",
"async": "^3.2.0",
"await-sleep": "0.0.1",
"aws-sdk": "^2.610.0",
"babel-eslint": "^10.1.0",

Просмотреть файл

@ -12,6 +12,7 @@ const patterns = require('../lib/patterns')
const authenticateToAWS = require('../lib/authenticate-to-aws.js')
const readlineSync = require('readline-sync')
const { execSync } = require('child_process')
const enterpriseServerVersions = Object.keys(allVersions).filter(v => v.startsWith('enterprise-server@'))
const uploadScript = path.join(process.cwd(), 'script/upload-images-to-s3.js')
// ignore the non-enterprise default version
@ -51,7 +52,8 @@ async function main () {
page,
site: siteData,
currentVersion: version,
currentLanguage: 'en'
currentLanguage: 'en',
enterpriseServerVersions
}
const rendered = await renderContent(page.markdown, context)

Просмотреть файл

@ -0,0 +1,41 @@
#!/usr/bin/env bash
# [start-readme]
#
# This script is run on a writer's machine to create an Early Access branch that matches the current docs-internal branch.
#
# [end-readme]
set -e
# Get current branch name
currentBranch=$(git rev-parse --abbrev-ref HEAD)
if [ $currentBranch == "main" ]; then
echo "You cannot run this script on the 'main' branch. Checkout a new branch first."
exit 0
fi
# Go up a directory
pushd .. > /dev/null
if [ ! -d "docs-early-access" ]; then
echo "A 'docs-early-access' directory does not exist! Run script/early-access/clone-locally first."
popd > /dev/null
exit 0
fi
# Navigate to docs-early-access
cd docs-early-access
# Check out main and update
git checkout main
git pull origin main
# Create a branch with the current docs-internal branch name
git checkout -b $currentBranch
# Go back to the previous working directory
popd > /dev/null
echo -e "\nDone! Created a branch called ${currentBranch}. Remember to commit your work in ../docs-early-access when you're ready."

Просмотреть файл

@ -18,14 +18,8 @@ if (!module.parent) {
if (status === false) {
// If in a deployed environment, warm the server at the start
if (process.env.NODE_ENV === 'production') {
// If in a true production environment, wait for the cache to be fully warmed.
if (process.env.HEROKU_PRODUCTION_APP) {
// If in a production environment, wait for the cache to be fully warmed.
await warmServer()
} else {
// If not in a true production environment, don't wait for the cache to be fully warmed.
// This avoids deployment timeouts in environments with slower servers.
warmServer()
}
}
// workaround for https://github.com/expressjs/express/issues/1101

Просмотреть файл

@ -87,7 +87,7 @@
/* Breadcrumbs
------------------------------------------------------------------------------*/
.breadcrumbs a:not(:last-child)::after{
.breadcrumbs a:not(:last-child)::after, .breadcrumbs span:not(:last-child)::after {
content: '/';
color: $gray-400;
padding-right: $spacer-1;

Просмотреть файл

@ -3,6 +3,8 @@ const sleep = require('await-sleep')
const querystring = require('querystring')
describe('homepage', () => {
jest.setTimeout(60 * 1000)
test('should be titled "GitHub Documentation"', async () => {
await page.goto('http://localhost:4001')
await expect(page.title()).resolves.toMatch('GitHub Documentation')
@ -10,6 +12,8 @@ describe('homepage', () => {
})
describe('algolia browser search', () => {
jest.setTimeout(60 * 1000)
it('works on the homepage', async () => {
await page.goto('http://localhost:4001/en')
await page.click('#search-input-container input[type="search"]')

Просмотреть файл

@ -42,7 +42,7 @@ describe('category pages', () => {
// Get links included in product index page.
// Each link corresponds to a product subdirectory (category).
// Example: "getting-started-with-github"
const contents = fs.readFileSync(productIndex, 'utf8')
const contents = fs.readFileSync(productIndex, 'utf8') // TODO move to async
const { content } = matter(contents)
const productDir = path.dirname(productIndex)
@ -50,6 +50,7 @@ describe('category pages', () => {
const categoryLinks = getLinks(content)
// Only include category directories, not standalone category files like content/actions/quickstart.md
.filter(link => fs.existsSync(getPath(productDir, link, 'index')))
// TODO this should move to async, but you can't asynchronously define tests with Jest...
// Map those to the Markdown file paths that represent that category page index
const categoryPaths = categoryLinks.map(link => getPath(productDir, link, 'index'))

Просмотреть файл

@ -4,6 +4,8 @@ const ignoredPagePaths = config.files[0].ignore
const ignoredDataPaths = config.files[2].ignore
describe('crowdin.yml config file', () => {
jest.setTimeout(60 * 1000)
let pages
beforeAll(async (done) => {
pages = await loadPages()

Просмотреть файл

@ -9,7 +9,10 @@ describe('glossaries', () => {
test('are broken into external, internal, and candidates', async () => {
const keys = Object.keys(glossaries)
expect(keys).toEqual(['candidates', 'external', 'internal'])
expect(keys).toHaveLength(3)
expect(keys).toContain('candidates')
expect(keys).toContain('external')
expect(keys).toContain('internal')
})
test('every entry has a valid term', async () => {

Просмотреть файл

@ -1,4 +1,4 @@
const fs = require('fs')
const fs = require('fs').promises
const path = require('path')
const cheerio = require('cheerio')
const matter = require('gray-matter')
@ -35,8 +35,8 @@ function processFrontmatter (contents, file) {
}
describe('removing liquid statements only', () => {
test('removes liquid statements that specify "greater than version to deprecate"', () => {
let contents = fs.readFileSync(greaterThan, 'utf8')
test('removes liquid statements that specify "greater than version to deprecate"', async () => {
let contents = await fs.readFile(greaterThan, 'utf8')
contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion)
const $ = cheerio.load(contents)
expect($('.example1').text().trim()).toBe('Alpha')
@ -57,8 +57,8 @@ Alpha\n\n{% else %}\n\nBravo\n\n{% if currentVersion ver_gt "enterprise-server@2
expect($('.example10').text().trim()).toBe('Alpha')
})
test('removes liquid statements that specify "and greater than version to deprecate"', () => {
let contents = fs.readFileSync(andGreaterThan1, 'utf8')
test('removes liquid statements that specify "and greater than version to deprecate"', async () => {
let contents = await fs.readFile(andGreaterThan1, 'utf8')
contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion)
const $ = cheerio.load(contents)
expect($('.example1').text().trim()).toBe('{% if currentVersion != "free-pro-team@latest" %}\n\nAlpha\n\n{% endif %}')
@ -71,8 +71,8 @@ Alpha\n\n{% if currentVersion != "free-pro-team@latest" %}\n\nBravo\n\n{% endif
Alpha\n\n{% if currentVersion ver_gt "enterprise-server@2.16" %}\n\nBravo\n\n{% endif %}\n\n{% endif %}`)
})
test('removes liquid statements that specify "and greater than version to deprecate" (alternate format)', () => {
let contents = fs.readFileSync(andGreaterThan2, 'utf8')
test('removes liquid statements that specify "and greater than version to deprecate" (alternate format)', async () => {
let contents = await fs.readFile(andGreaterThan2, 'utf8')
contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion)
const $ = cheerio.load(contents)
expect($('.example1').text().trim()).toBe('{% if currentVersion ver_lt "enterprise-server@2.16" %}\n\nAlpha\n\n{% endif %}')
@ -85,8 +85,8 @@ Alpha\n\n{% if currentVersion ver_lt "enterprise-server@2.16" %}\n\nBravo\n\n{%
Alpha\n\n{% if currentVersion != "free-pro-team@latest" %}\n\nBravo\n\n{% endif %}\n\n{% endif %}`)
})
test('removes liquid statements that specify "not equals version to deprecate"', () => {
let contents = fs.readFileSync(notEquals, 'utf8')
test('removes liquid statements that specify "not equals version to deprecate"', async () => {
let contents = await fs.readFile(notEquals, 'utf8')
contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion)
const $ = cheerio.load(contents)
expect($('.example1').text().trim()).toBe('Alpha')
@ -103,8 +103,8 @@ Alpha\n\n{% endif %}`)
})
describe('removing liquid statements and content', () => {
test('removes interior content and liquid statements that specify "equals version to deprecate"', () => {
let contents = fs.readFileSync(equals, 'utf8')
test('removes interior content and liquid statements that specify "equals version to deprecate"', async () => {
let contents = await fs.readFile(equals, 'utf8')
contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion)
const $ = cheerio.load(contents)
expect($('.example1').text().trim()).toBe('')
@ -117,8 +117,8 @@ Alpha\n\n{% else %}\n\nCharlie\n\n{% endif %}`)
expect($('.example6').text().trim()).toBe('Charlie\n\nBravo')
})
test('removes interior content and liquid statements that specify "less than next oldest than version to deprecate"', () => {
let contents = fs.readFileSync(lessThanNextOldest, 'utf8')
test('removes interior content and liquid statements that specify "less than next oldest than version to deprecate"', async () => {
let contents = await fs.readFile(lessThanNextOldest, 'utf8')
contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion)
const $ = cheerio.load(contents)
expect($('.example1').text().trim()).toBe('Alpha')
@ -137,8 +137,8 @@ Charlie\n\n{% else %}\n\nDelta\n\n{% endif %}\n\nEcho`)
})
describe('updating frontmatter', () => {
test('updates frontmatter versions Enterprise if set to greater-than-or-equal-to version to deprecate', () => {
let contents = fs.readFileSync(frontmatter1, 'utf8')
test('updates frontmatter versions Enterprise if set to greater-than-or-equal-to version to deprecate', async () => {
let contents = await fs.readFile(frontmatter1, 'utf8')
contents = processFrontmatter(contents, frontmatter1)
const $ = cheerio.load(contents)
// console.log('foo')
@ -147,8 +147,8 @@ describe('updating frontmatter', () => {
expect($.text().includes('enterprise-server: \'>=2.13\'')).toBe(false)
})
test('updates frontmatter versions Enterprise if set to greater-than-or-equal-to next oldest version', () => {
let contents = fs.readFileSync(frontmatter2, 'utf8')
test('updates frontmatter versions Enterprise if set to greater-than-or-equal-to next oldest version', async () => {
let contents = await fs.readFile(frontmatter2, 'utf8')
contents = processFrontmatter(contents, frontmatter2)
const $ = cheerio.load(contents)
expect($.text().includes('enterprise-server: \'*\'')).toBe(true)
@ -157,8 +157,8 @@ describe('updating frontmatter', () => {
})
describe('whitespace', () => {
test('does not add newlines when whitespace control is used', () => {
let contents = fs.readFileSync(whitespace, 'utf8')
test('does not add newlines when whitespace control is used', async () => {
let contents = await fs.readFile(whitespace, 'utf8')
contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion)
const $ = cheerio.load(contents)
expect($('.example1').text()).toBe('\n Alpha\n')
@ -167,8 +167,8 @@ describe('whitespace', () => {
expect($('.example4').text()).toBe('\n Alpha\n')
})
test('does not add newlines when no newlines are present', () => {
let contents = fs.readFileSync(whitespace, 'utf8')
test('does not add newlines when no newlines are present', async () => {
let contents = await fs.readFile(whitespace, 'utf8')
contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion)
const $ = cheerio.load(contents)
expect($('.example5').text()).toBe('\n Alpha\n')

Просмотреть файл

@ -3,10 +3,12 @@ const loadSiteData = require('../../lib/site-data')
const { loadPages } = require('../../lib/pages')
const getDataReferences = require('../../lib/get-liquid-data-references')
const frontmatter = require('@github-docs/frontmatter')
const fs = require('fs')
const fs = require('fs').promises
const path = require('path')
describe('data references', () => {
jest.setTimeout(60 * 1000)
let data, pages
beforeAll(async (done) => {
@ -33,34 +35,34 @@ describe('data references', () => {
expect(errors.length, JSON.stringify(errors, null, 2)).toBe(0)
})
test('every data reference found in metadata of English content files is defined and has a value', () => {
test('every data reference found in metadata of English content files is defined and has a value', async () => {
let errors = []
expect(pages.length).toBeGreaterThan(0)
pages.forEach(page => {
await Promise.all(pages.map(async page => {
const metadataFile = path.join('content', page.relativePath)
const fileContents = fs.readFileSync(path.join(__dirname, '../..', metadataFile))
const fileContents = await fs.readFile(path.join(__dirname, '../..', metadataFile))
const { data: metadata } = frontmatter(fileContents, { filepath: page.fullPath })
const metadataRefs = getDataReferences(JSON.stringify(metadata))
metadataRefs.forEach(key => {
const value = get(data.en, key)
if (typeof value !== 'string') errors.push({ key, value, metadataFile })
})
})
}))
errors = uniqWith(errors, isEqual) // remove duplicates
expect(errors.length, JSON.stringify(errors, null, 2)).toBe(0)
})
test('every data reference found in English reusable files is defined and has a value', () => {
test('every data reference found in English reusable files is defined and has a value', async () => {
let errors = []
const allReusables = data.en.site.data.reusables
const reusables = Object.values(allReusables)
expect(reusables.length).toBeGreaterThan(0)
reusables.forEach(reusablesPerFile => {
await Promise.all(reusables.map(async reusablesPerFile => {
let reusableFile = path.join(__dirname, '../../data/reusables/', getFilenameByValue(allReusables, reusablesPerFile))
reusableFile = getFilepath(reusableFile)
reusableFile = await getFilepath(reusableFile)
const reusableRefs = getDataReferences(JSON.stringify(reusablesPerFile))
@ -68,21 +70,21 @@ describe('data references', () => {
const value = get(data.en, key)
if (typeof value !== 'string') errors.push({ key, value, reusableFile })
})
})
}))
errors = uniqWith(errors, isEqual) // remove duplicates
expect(errors.length, JSON.stringify(errors, null, 2)).toBe(0)
})
test('every data reference found in English variable files is defined and has a value', () => {
test('every data reference found in English variable files is defined and has a value', async () => {
let errors = []
const allVariables = data.en.site.data.variables
const variables = Object.values(allVariables)
expect(variables.length).toBeGreaterThan(0)
variables.forEach(variablesPerFile => {
await Promise.all(variables.map(async variablesPerFile => {
let variableFile = path.join(__dirname, '../../data/variables/', getFilenameByValue(allVariables, variablesPerFile))
variableFile = getFilepath(variableFile)
variableFile = await getFilepath(variableFile)
const variableRefs = getDataReferences(JSON.stringify(variablesPerFile))
@ -90,7 +92,7 @@ describe('data references', () => {
const value = get(data.en, key)
if (typeof value !== 'string') errors.push({ key, value, variableFile })
})
})
}))
errors = uniqWith(errors, isEqual) // remove duplicates
expect(errors.length, JSON.stringify(errors, null, 2)).toBe(0)
@ -102,10 +104,13 @@ function getFilenameByValue (object, value) {
}
// if path exists, assume it's a directory; otherwise, assume a YML extension
function getFilepath (filepath) {
filepath = fs.existsSync(filepath)
? filepath + '/'
: filepath + '.yml'
async function getFilepath (filepath) {
try {
await fs.stat(filepath)
filepath = filepath + '/'
} catch (_) {
filepath = filepath + '.yml'
}
// we only need the relative path
return filepath.replace(path.join(__dirname, '../../'), '')

Просмотреть файл

@ -45,11 +45,11 @@ describe('siteData module (English)', () => {
// TODO: re-enable once Janky flakyness is resolved
test.skip('backfills missing translated site data with English values', async () => {
const newFile = path.join(__dirname, '../../data/newfile.yml')
fs.writeFileSync(newFile, 'newvalue: bar')
await fs.writeFile(newFile, 'newvalue: bar')
const data = await loadSiteData()
expect(get(data, 'en.site.data.newfile.newvalue')).toEqual('bar')
expect(get(data, 'ja.site.data.newfile.newvalue')).toEqual('bar')
fs.unlinkSync(newFile)
await fs.unlink(newFile)
})
test('all Liquid templating is valid', async () => {

22
tests/fixtures/rest-redirects.json поставляемый
Просмотреть файл

@ -219,7 +219,7 @@
"/en/enterprise/2.20/v3/enterprise-admin/orgs": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#orgs",
"/en/enterprise/2.20/v3/enterprise-admin/pre_receive_environments": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#pre-receive-environments",
"/en/enterprise/2.20/v3/enterprise-admin/pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#pre-receive-hooks",
"/en/enterprise/2.20/v3/enterprise-admin/repo_pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#repo-pre-receive-hooks",
"/en/enterprise/2.20/v3/enterprise-admin/repo_pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#repository-pre-receive-hooks",
"/en/enterprise/2.20/v3/enterprise-admin/search_indexing": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#search-indexing",
"/en/enterprise/2.20/v3/enterprise-admin/users": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#users",
"/en/enterprise/2.20/v3/gists/comments": "/en/enterprise-server@2.20/rest/reference/gists#comments",
@ -243,7 +243,7 @@
"/en/enterprise/2.20/v3/markdown": "/en/enterprise-server@2.20/rest/reference/markdown",
"/en/enterprise/2.20/v3/meta": "/en/enterprise-server@2.20/rest/reference/meta",
"/en/enterprise/2.20/v3/oauth_authorizations": "/en/enterprise-server@2.20/rest/reference/oauth-authorizations",
"/en/enterprise/2.20/v3/orgs/hooks": "/en/enterprise-server@2.20/rest/reference/orgs#hooks",
"/en/enterprise/2.20/v3/orgs/hooks": "/en/enterprise-server@2.20/rest/reference/orgs#webhooks",
"/en/enterprise/2.20/v3/orgs": "/en/enterprise-server@2.20/rest/reference/orgs",
"/en/enterprise/2.20/v3/orgs/members": "/en/enterprise-server@2.20/rest/reference/orgs#members",
"/en/enterprise/2.20/v3/orgs/outside_collaborators": "/en/enterprise-server@2.20/rest/reference/orgs#outside-collaborators",
@ -267,13 +267,13 @@
"/en/enterprise/2.20/v3/repos/deployments": "/en/enterprise-server@2.20/rest/reference/repos#deployments",
"/en/enterprise/2.20/v3/repos/downloads": "/en/enterprise-server@2.20/rest/reference/repos#downloads",
"/en/enterprise/2.20/v3/repos/forks": "/en/enterprise-server@2.20/rest/reference/repos#forks",
"/en/enterprise/2.20/v3/repos/hooks": "/en/enterprise-server@2.20/rest/reference/repos#hooks",
"/en/enterprise/2.20/v3/repos/hooks": "/en/enterprise-server@2.20/rest/reference/repos#webhooks",
"/en/enterprise/2.20/v3/repos": "/en/enterprise-server@2.20/rest/reference/repos",
"/en/enterprise/2.20/v3/repos/invitations": "/en/enterprise-server@2.20/rest/reference/repos#invitations",
"/en/enterprise/2.20/v3/repos/keys": "/en/enterprise-server@2.20/rest/reference/repos#keys",
"/en/enterprise/2.20/v3/repos/merging": "/en/enterprise-server@2.20/rest/reference/repos#merging",
"/en/enterprise/2.20/v3/repos/pages": "/en/enterprise-server@2.20/rest/reference/repos#pages",
"/en/enterprise/2.20/v3/repos/pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/repos#pre-receive-hooks",
"/en/enterprise/2.20/v3/repos/pre_receive_hooks": "/en/enterprise-server@2.20/rest/reference/enterprise-admin#repository-pre-receive-hooks",
"/en/enterprise/2.20/v3/repos/releases": "/en/enterprise-server@2.20/rest/reference/repos#releases",
"/en/enterprise/2.20/v3/repos/statistics": "/en/enterprise-server@2.20/rest/reference/repos#statistics",
"/en/enterprise/2.20/v3/repos/statuses": "/en/enterprise-server@2.20/rest/reference/repos#statuses",
@ -316,7 +316,7 @@
"/en/enterprise/2.21/v3/enterprise-admin/orgs": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#orgs",
"/en/enterprise/2.21/v3/enterprise-admin/pre_receive_environments": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#pre-receive-environments",
"/en/enterprise/2.21/v3/enterprise-admin/pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#pre-receive-hooks",
"/en/enterprise/2.21/v3/enterprise-admin/repo_pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#repo-pre-receive-hooks",
"/en/enterprise/2.21/v3/enterprise-admin/repo_pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#repository-pre-receive-hooks",
"/en/enterprise/2.21/v3/enterprise-admin/search_indexing": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#search-indexing",
"/en/enterprise/2.21/v3/enterprise-admin/users": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#users",
"/en/enterprise/2.21/v3/gists/comments": "/en/enterprise-server@2.21/rest/reference/gists#comments",
@ -340,7 +340,7 @@
"/en/enterprise/2.21/v3/markdown": "/en/enterprise-server@2.21/rest/reference/markdown",
"/en/enterprise/2.21/v3/meta": "/en/enterprise-server@2.21/rest/reference/meta",
"/en/enterprise/2.21/v3/oauth_authorizations": "/en/enterprise-server@2.21/rest/reference/oauth-authorizations",
"/en/enterprise/2.21/v3/orgs/hooks": "/en/enterprise-server@2.21/rest/reference/orgs#hooks",
"/en/enterprise/2.21/v3/orgs/hooks": "/en/enterprise-server@2.21/rest/reference/orgs#webhooks",
"/en/enterprise/2.21/v3/orgs": "/en/enterprise-server@2.21/rest/reference/orgs",
"/en/enterprise/2.21/v3/orgs/members": "/en/enterprise-server@2.21/rest/reference/orgs#members",
"/en/enterprise/2.21/v3/orgs/outside_collaborators": "/en/enterprise-server@2.21/rest/reference/orgs#outside-collaborators",
@ -364,13 +364,13 @@
"/en/enterprise/2.21/v3/repos/deployments": "/en/enterprise-server@2.21/rest/reference/repos#deployments",
"/en/enterprise/2.21/v3/repos/downloads": "/en/enterprise-server@2.21/rest/reference/repos#downloads",
"/en/enterprise/2.21/v3/repos/forks": "/en/enterprise-server@2.21/rest/reference/repos#forks",
"/en/enterprise/2.21/v3/repos/hooks": "/en/enterprise-server@2.21/rest/reference/repos#hooks",
"/en/enterprise/2.21/v3/repos/hooks": "/en/enterprise-server@2.21/rest/reference/repos#webhooks",
"/en/enterprise/2.21/v3/repos": "/en/enterprise-server@2.21/rest/reference/repos",
"/en/enterprise/2.21/v3/repos/invitations": "/en/enterprise-server@2.21/rest/reference/repos#invitations",
"/en/enterprise/2.21/v3/repos/keys": "/en/enterprise-server@2.21/rest/reference/repos#keys",
"/en/enterprise/2.21/v3/repos/merging": "/en/enterprise-server@2.21/rest/reference/repos#merging",
"/en/enterprise/2.21/v3/repos/pages": "/en/enterprise-server@2.21/rest/reference/repos#pages",
"/en/enterprise/2.21/v3/repos/pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/repos#pre-receive-hooks",
"/en/enterprise/2.21/v3/repos/pre_receive_hooks": "/en/enterprise-server@2.21/rest/reference/enterprise-admin#repository-pre-receive-hooks",
"/en/enterprise/2.21/v3/repos/releases": "/en/enterprise-server@2.21/rest/reference/repos#releases",
"/en/enterprise/2.21/v3/repos/statistics": "/en/enterprise-server@2.21/rest/reference/repos#statistics",
"/en/enterprise/2.21/v3/repos/statuses": "/en/enterprise-server@2.21/rest/reference/repos#statuses",
@ -453,7 +453,7 @@
"/v3/migrations/users": "/en/free-pro-team@latest/rest/reference/migrations#users",
"/v3/oauth_authorizations": "/en/enterprise-server/rest/reference/oauth-authorizations",
"/v3/orgs/blocking": "/en/free-pro-team@latest/rest/reference/orgs#blocking",
"/v3/orgs/hooks": "/en/free-pro-team@latest/rest/reference/orgs#hooks",
"/v3/orgs/hooks": "/en/free-pro-team@latest/rest/reference/orgs#webhooks",
"/v3/orgs": "/en/free-pro-team@latest/rest/reference/orgs",
"/v3/orgs/members": "/en/free-pro-team@latest/rest/reference/orgs#members",
"/v3/orgs/migrations": "/en/free-pro-team@latest/rest/reference/orgs#migrations",
@ -479,13 +479,13 @@
"/v3/repos/deployments": "/en/free-pro-team@latest/rest/reference/repos#deployments",
"/v3/repos/downloads": "/en/free-pro-team@latest/rest/reference/repos#downloads",
"/v3/repos/forks": "/en/free-pro-team@latest/rest/reference/repos#forks",
"/v3/repos/hooks": "/en/free-pro-team@latest/rest/reference/repos#hooks",
"/v3/repos/hooks": "/en/free-pro-team@latest/rest/reference/repos#webhooks",
"/v3/repos": "/en/free-pro-team@latest/rest/reference/repos",
"/v3/repos/invitations": "/en/free-pro-team@latest/rest/reference/repos#invitations",
"/v3/repos/keys": "/en/free-pro-team@latest/rest/reference/repos#keys",
"/v3/repos/merging": "/en/free-pro-team@latest/rest/reference/repos#merging",
"/v3/repos/pages": "/en/free-pro-team@latest/rest/reference/repos#pages",
"/v3/repos/pre_receive_hooks": "/en/free-pro-team@latest/rest/reference/repos#pre-receive-hooks",
"/v3/repos/pre_receive_hooks": "/en/enterprise-server/rest/reference/enterprise-admin#repository-pre-receive-hooks",
"/v3/repos/releases": "/en/free-pro-team@latest/rest/reference/repos#releases",
"/v3/repos/statistics": "/en/free-pro-team@latest/rest/reference/repos#statistics",
"/v3/repos/statuses": "/en/free-pro-team@latest/rest/reference/repos#statuses",

Просмотреть файл

@ -1,6 +1,6 @@
const yaml = require('js-yaml')
const { createChangelogEntry, cleanPreviewTitle, previewAnchor, prependDatedEntry } = require('../../script/graphql/build-changelog')
const fs = require('fs')
const fs = require('fs').promises
const MockDate = require('mockdate')
const expectedChangelogEntry = require('../fixtures/changelog-entry')
const expectedUpdatedChangelogFile = require('../fixtures/updated-changelog-file')
@ -111,18 +111,18 @@ describe('updating the changelog file', () => {
MockDate.reset()
})
it('modifies the entry object and the file on disk', () => {
it('modifies the entry object and the file on disk', async () => {
const testTargetPath = 'tests/graphql/example_changelog.json'
const previousContents = fs.readFileSync(testTargetPath)
const previousContents = await fs.readFile(testTargetPath)
const exampleEntry = { someStuff: true }
const expectedDate = '2020-11-20'
MockDate.set(expectedDate)
prependDatedEntry(exampleEntry, testTargetPath)
const newContents = fs.readFileSync(testTargetPath, 'utf8')
const newContents = await fs.readFile(testTargetPath, 'utf8')
// reset the file:
fs.writeFileSync(testTargetPath, previousContents)
await fs.writeFile(testTargetPath, previousContents)
expect(exampleEntry).toEqual({ someStuff: true, date: expectedDate })
expect(JSON.parse(newContents)).toEqual(expectedUpdatedChangelogFile)

Просмотреть файл

@ -0,0 +1,9 @@
const runningActionsOnInternalRepo = process.env.GITHUB_ACTIONS === 'true' && process.env.GITHUB_REPOSITORY === 'github/docs-internal'
const testViaActionsOnly = runningActionsOnInternalRepo ? test : test.skip
const describeViaActionsOnly = runningActionsOnInternalRepo ? describe : describe.skip
module.exports = {
testViaActionsOnly,
describeViaActionsOnly
}

Просмотреть файл

@ -1,20 +1,28 @@
const fs = require('fs')
const fs = require('fs').promises
const path = require('path')
const { filter: asyncFilter } = require('async')
describe('check for orphan tests', () => {
test('all tests are in sub-directories', () => {
test('all tests are in sub-directories', async () => {
// A known list of exceptions that can live outside of directories
const EXCEPTIONS = ['README.md']
const pathToTests = path.join(process.cwd(), 'tests')
// Get a list of files/directories in `/tests`
const testDirectory = fs.readdirSync(pathToTests)
const testDirectory = await fs.readdir(pathToTests)
const filteredList = testDirectory
let filteredList = testDirectory
// Filter out our exceptions
.filter(item => !EXCEPTIONS.includes(item))
// Don't include directories
.filter(item => !fs.statSync(path.join(pathToTests, item)).isDirectory())
filteredList = await asyncFilter(
filteredList,
async item => !(
await fs.stat(
path.join(pathToTests, item)
)
).isDirectory()
)
expect(filteredList).toHaveLength(0)
})

Просмотреть файл

@ -1,6 +1,8 @@
const { getDOM, getJSON } = require('../helpers/supertest')
const nonEnterpriseDefaultVersion = require('../../lib/non-enterprise-default-version')
const describeInternalOnly = process.env.GITHUB_REPOSITORY === 'github/docs-internal' ? describe : describe.skip
describe('breadcrumbs', () => {
jest.setTimeout(300 * 1000)
@ -60,6 +62,27 @@ describe('breadcrumbs', () => {
})
})
describeInternalOnly('early access rendering', () => {
test('top-level product pages have breadcrumbs', async () => {
const $ = await getDOM('/early-access/github/articles/using-gist-playground')
expect($('.breadcrumbs')).toHaveLength(1)
})
test('early access article pages have breadcrumbs with product, category, and article', async () => {
const $ = await getDOM('/early-access/github/enforcing-best-practices-with-github-policies/about-github-policies')
const $breadcrumbSpans = $('.breadcrumbs span')
const $breadcrumbLinks = $('.breadcrumbs a')
expect($breadcrumbSpans).toHaveLength(2)
expect($breadcrumbLinks).toHaveLength(2)
expect($breadcrumbSpans.eq(0).text()).toBe('Early Access documentation')
expect($breadcrumbSpans.eq(1).text()).toBe('GitHub.com')
expect($breadcrumbLinks.eq(0).attr('title')).toBe('category: Enforcing best practices with GitHub Policies')
expect($breadcrumbLinks.eq(1).attr('title')).toBe('article: About GitHub Policies')
expect($breadcrumbLinks.eq(1).hasClass('text-gray-light')).toBe(true)
})
})
describe('context.breadcrumbs object', () => {
test('works on product index pages', async () => {
const breadcrumbs = await getJSON('/en/github?json=breadcrumbs')

Просмотреть файл

@ -1,4 +1,4 @@
const fs = require('fs')
const fs = require('fs').promises
const path = require('path')
const { difference, isPlainObject } = require('lodash')
const { getJSON } = require('../helpers/supertest')
@ -17,7 +17,7 @@ describe('REST references docs', () => {
test('markdown file exists for every operationId prefix in the api.github.com schema', async () => {
const { categories } = require('../../lib/rest')
const referenceDir = path.join(__dirname, '../../content/rest/reference')
const filenames = fs.readdirSync(referenceDir)
const filenames = (await fs.readdir(referenceDir))
.filter(filename => !excludeFromResourceNameCheck.find(excludedFile => filename.endsWith(excludedFile)))
.map(filename => filename.replace('.md', ''))

Просмотреть файл

@ -1,6 +1,7 @@
const lodash = require('lodash')
const enterpriseServerReleases = require('../../lib/enterprise-server-releases')
const { get, getDOM, head } = require('../helpers/supertest')
const { describeViaActionsOnly } = require('../helpers/conditional-runs')
const path = require('path')
const nonEnterpriseDefaultVersion = require('../../lib/non-enterprise-default-version')
const { loadPages } = require('../../lib/pages')
@ -356,7 +357,7 @@ describe('server', () => {
})
})
describe.skip('Early Access articles', () => {
describeViaActionsOnly('Early Access articles', () => {
let hiddenPageHrefs, hiddenPages
beforeAll(async (done) => {

Просмотреть файл

@ -1,7 +1,6 @@
const { eachOfLimit } = require('async')
const enterpriseServerReleases = require('../../lib/enterprise-server-releases')
const { get } = require('../helpers/supertest')
const { getEnterpriseVersionNumber } = require('../../lib/patterns')
const nonEnterpriseDefaultVersion = require('../../lib/non-enterprise-default-version')
const restRedirectFixtures = require('../fixtures/rest-redirects')
const graphqlRedirectFixtures = require('../fixtures/graphql-redirects')
@ -126,29 +125,6 @@ describe('developer redirects', () => {
)
})
// TODO temprarily ensure we redirect old links using the new enterprise format
// for currently supported enterprise releases only
// EXAMPLE: /en/enterprise-server@2.20/v3/pulls/comments -> /en/enterprise-server@2.20/rest/reference/pulls#comments
// We can remove test after we update all the old `/v3` links to point to `/rest`
test('temporary rest reference enterprise redirects', async () => {
await eachOfLimit(
restRedirectFixtures,
MAX_CONCURRENT_REQUESTS,
async (newPath, oldPath) => {
const releaseNumber = oldPath.match(getEnterpriseVersionNumber)
if (!releaseNumber) return
if (!enterpriseServerReleases.supported.includes(releaseNumber[1])) return
oldPath = oldPath
.replace(/\/enterprise\/(\d.\d\d)\//, '/enterprise-server@$1/')
.replace('/user/', '/')
const res = await get(oldPath)
expect(res.statusCode, `${oldPath} did not redirect to ${newPath}`).toBe(301)
expect(res.headers.location).toBe(newPath)
}
)
})
// this fixtures file includes /v4 and /enterprise/v4 paths
test('graphql reference redirects', async () => {
await eachOfLimit(
@ -164,28 +140,5 @@ describe('developer redirects', () => {
}
)
})
// TODO temprarily ensure we redirect old links using the new enterprise format
// for currently supported enterprise releases only
// EXAMPLE: /en/enterprise-server@2.20/v4/interface/actor -> /en/enterprise-server@2.20/graphql/reference/interfaces#actor
// We can remove test after we update all the old `/v4` links to point to `/graphql`
test('temporary rest reference enterprise redirects', async () => {
await eachOfLimit(
graphqlRedirectFixtures,
MAX_CONCURRENT_REQUESTS,
async (newPath, oldPath) => {
const releaseNumber = oldPath.match(getEnterpriseVersionNumber)
if (!releaseNumber) return
if (!enterpriseServerReleases.supported.includes(releaseNumber[1])) return
oldPath = oldPath
.replace(/\/enterprise\/(\d.\d\d)\//, '/enterprise-server@$1/')
.replace('/user/', '/')
const res = await get(oldPath)
expect(res.statusCode, `${oldPath} did not redirect to ${newPath}`).toBe(301)
expect(res.headers.location).toBe(newPath)
}
)
})
})
})

Просмотреть файл

@ -17,8 +17,8 @@ describe('redirects', () => {
done()
})
test('page.redirects is an array', () => {
const page = new Page({
test('page.redirects is an array', async () => {
const page = await Page.init({
relativePath: 'github/collaborating-with-issues-and-pull-requests/about-branches.md',
basePath: path.join(__dirname, '../../content'),
languageCode: 'en'
@ -26,8 +26,8 @@ describe('redirects', () => {
expect(isPlainObject(page.redirects)).toBe(true)
})
test('dotcom homepage page.redirects', () => {
const page = new Page({
test('dotcom homepage page.redirects', async () => {
const page = await Page.init({
relativePath: 'github/index.md',
basePath: path.join(__dirname, '../../content'),
languageCode: 'en'
@ -41,7 +41,7 @@ describe('redirects', () => {
})
test('converts single `redirect_from` strings values into arrays', async () => {
const page = new Page({
const page = await Page.init({
relativePath: 'github/collaborating-with-issues-and-pull-requests/about-conversations-on-github.md',
basePath: path.join(__dirname, '../../content'),
languageCode: 'en'

Просмотреть файл

@ -0,0 +1,15 @@
const filenameToKey = require('../../../lib/filename-to-key')
describe('filename-to-key', () => {
test('converts filenames to object keys', () => {
expect(filenameToKey('foo/bar/baz.txt')).toBe('foo.bar.baz')
})
test('ignores leading slash on filenames', () => {
expect(filenameToKey('/foo/bar/baz.txt')).toBe('foo.bar.baz')
})
test('supports MS Windows paths', () => {
expect(filenameToKey('path\\to\\file.txt')).toBe('path.to.file')
})
})

Просмотреть файл

@ -0,0 +1 @@
I am a README. I am ignored by default.

Просмотреть файл

@ -0,0 +1 @@
another_markup_language: 'yes'

Просмотреть файл

@ -0,0 +1 @@
{"meaningOfLife": 42}

Просмотреть файл

@ -0,0 +1 @@
I am markdown!

Просмотреть файл

@ -0,0 +1,40 @@
const path = require('path')
const dataDirectory = require('../../../lib/data-directory')
const fixturesDir = path.join(__dirname, 'fixtures')
describe('data-directory', () => {
test('works', async () => {
const data = await dataDirectory(fixturesDir)
const expected = {
bar: { another_markup_language: 'yes' },
foo: { meaningOfLife: 42 },
nested: { baz: 'I am markdown!' }
}
expect(data).toEqual(expected)
})
test('option: preprocess function', async () => {
const preprocess = function (content) {
return content.replace('markdown', 'MARKDOWN')
}
const data = await dataDirectory(fixturesDir, { preprocess })
expect(data.nested.baz).toBe('I am MARKDOWN!')
})
test('option: extensions array', async () => {
const extensions = ['.yaml', 'markdown']
const data = await dataDirectory(fixturesDir, { extensions })
expect('bar' in data).toBe(true)
expect('foo' in data).toBe(false) // JSON file should be ignored
})
test('option: ignorePatterns', async () => {
const ignorePatterns = []
// README is ignored by default
expect('README' in await dataDirectory(fixturesDir)).toBe(false)
// README can be included by setting empty ignorePatterns array
expect('README' in await dataDirectory(fixturesDir, { ignorePatterns })).toBe(true)
})
})

Просмотреть файл

@ -1,23 +1,20 @@
const fs = require('fs')
const fs = require('fs').promises
const path = require('path')
const { GITHUB_ACTIONS, GITHUB_REPOSITORY } = process.env
const runningActionsOnInternalRepo = GITHUB_ACTIONS === 'true' && GITHUB_REPOSITORY === 'github/docs-internal'
const testViaActionsOnly = runningActionsOnInternalRepo ? test : test.skip
const { testViaActionsOnly } = require('../helpers/conditional-runs')
describe('cloning early-access', () => {
testViaActionsOnly('the content directory exists', async () => {
const eaContentDir = path.join(process.cwd(), 'content/early-access')
expect(fs.existsSync(eaContentDir)).toBe(true)
const eaDir = path.join(process.cwd(), 'content/early-access')
expect(await fs.stat(eaDir)).toBeTruthy()
})
testViaActionsOnly('the data directory exists', async () => {
const eaContentDir = path.join(process.cwd(), 'data/early-access')
expect(fs.existsSync(eaContentDir)).toBe(true)
const eaDir = path.join(process.cwd(), 'data/early-access')
expect(await fs.stat(eaDir)).toBeTruthy()
})
testViaActionsOnly('the assets/images directory exists', async () => {
const eaContentDir = path.join(process.cwd(), 'assets/images/early-access')
expect(fs.existsSync(eaContentDir)).toBe(true)
const eaDir = path.join(process.cwd(), 'assets/images/early-access')
expect(await fs.stat(eaDir)).toBeTruthy()
})
})

Просмотреть файл

@ -8,7 +8,7 @@ describe('find page', () => {
jest.setTimeout(1000 * 1000)
test('falls back to the English page if it can\'t find a localized page', async () => {
const page = new Page({
const page = await Page.init({
relativePath: 'page-that-does-not-exist-in-translations-dir.md',
basePath: path.join(__dirname, '../fixtures'),
languageCode: 'en'
@ -24,7 +24,7 @@ describe('find page', () => {
})
test('follows redirects', async () => {
const page = new Page({
const page = await Page.init({
relativePath: 'page-with-redirects.md',
basePath: path.join(__dirname, '../fixtures'),
languageCode: 'en'

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше