Merge branch 'main' into video-uploads-ga
4
.babelrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": [["styled-components", { "ssr": true }]]
|
||||
}
|
||||
33
.github/ISSUE_COMMENT_TEMPLATE/batch-status.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Batch Status Update
|
||||
description: Batch Status Update
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Status
|
||||
options:
|
||||
- label: "GREEN \U0001F7E2 (All good, smooth sailing)"
|
||||
value: 'Status: GREEN'
|
||||
- label:
|
||||
"YELLOW \U0001F7E1 (We've identified areas of concern, but have them under
|
||||
control at this time)"
|
||||
value: 'Status: YELLOW'
|
||||
- label: "RED \U0001F534 (We need help, there are blockers beyond our control)"
|
||||
value: 'Status: RED'
|
||||
- label: GREY ⚪️ (Not started, paused, not currently being worked on)
|
||||
value: 'Status: GREY'
|
||||
- label: "BLACK ⚫️ (We shipped it \U0001F389)"
|
||||
value: 'Status: BLACK'
|
||||
- type: input
|
||||
attributes:
|
||||
label: Target date
|
||||
format: date
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Update
|
||||
placeholder: A few words on how it's going
|
||||
- type: input
|
||||
attributes:
|
||||
label: 'Attribution'
|
||||
value: '_created with :heart: by typing_ `/status`'
|
||||
format: text
|
||||
29
.github/ISSUE_COMMENT_TEMPLATE/quick-status.yml
vendored
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: A brief status update
|
||||
description: A brief status update.
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Status
|
||||
options:
|
||||
- label: 'GREY ⚪️ (Not started, paused, not currently being worked on)'
|
||||
value: 'Status: GREY'
|
||||
- label: 'GREEN 🟢 (All good, smooth sailing)'
|
||||
value: 'Status: GREEN'
|
||||
- label: "YELLOW \U0001F7E1 (On track, with hurdles to work through)"
|
||||
value: 'Status: YELLOW'
|
||||
- label: "RED \U0001F534 (BLOCKED)"
|
||||
value: 'Status: RED'
|
||||
- label: "BLACK ⚫️ (We shipped it \U0001F389)"
|
||||
value: 'Status: BLACK'
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Update Summary
|
||||
placeholder:
|
||||
Brief summary of the status and next steps. Any blockers should be
|
||||
called out specifically.
|
||||
- type: input
|
||||
attributes:
|
||||
label: 'Attribution'
|
||||
value: '_created with :heart: by typing_ `/status`'
|
||||
format: text
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -12,19 +12,20 @@ Thanks again!
|
||||
|
||||
### Why:
|
||||
|
||||
Closes [issue link]
|
||||
|
||||
<!--
|
||||
- If there's an existing issue for your change, please link to it.
|
||||
- If there's _not_ an existing issue, please open one first to make it more likely that this update will be accepted: https://github.com/github/docs/issues/new/choose. -->
|
||||
**Closes [issue link]**
|
||||
|
||||
### What's being changed:
|
||||
|
||||
<!-- Share artifacts of the changes, be they code snippets, GIFs or screenshots; whatever shares the most context. -->
|
||||
|
||||
### Check off the following:
|
||||
- [ ] I have reviewed my changes in staging. (look for the **deploy-to-heroku** link in your pull request, then click **View deployment**)
|
||||
- [ ] For content changes, I have reviewed the [localization checklist](https://github.com/github/docs/blob/main/contributing/localization-checklist.md)
|
||||
- [ ] For content changes, I have reviewed the [Content style guide for GitHub Docs](https://github.com/github/docs/blob/main/contributing/content-style-guide.md).
|
||||
|
||||
- [ ] I have reviewed my changes in staging (look for the **deploy-to-heroku** link in your pull request, then click **View deployment**).
|
||||
- [ ] For content changes, I have completed the [self-review checklist](https://github.com/github/docs/blob/main/CONTRIBUTING.md#self-review).
|
||||
|
||||
### Writer impact (This section is for GitHub staff members only):
|
||||
|
||||
|
||||
232
.github/actions-scripts/fr-add-docs-reviewers-requests.py
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
# TODO: Convert to JavaScript for language consistency
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
|
||||
# Constants
|
||||
endpoint = 'https://api.github.com/graphql'
|
||||
|
||||
# ID of the github/github repo
|
||||
github_repo_id = "MDEwOlJlcG9zaXRvcnkz"
|
||||
|
||||
# ID of the docs-reviewers team
|
||||
docs_reviewers_id = "MDQ6VGVhbTQzMDMxMzk="
|
||||
|
||||
# ID of the "Docs content first responder" board
|
||||
docs_project_id = "MDc6UHJvamVjdDQ1NzI0ODI="
|
||||
|
||||
# ID of the "OpenAPI review requests" column on the "Docs content first responder" board
|
||||
docs_column_id = "PC_lAPNJr_OAEXFQs4A2OFq"
|
||||
|
||||
# 100 is an educated guess of how many PRs are opened in a day on the github/github repo
|
||||
# If we are missing PRs, either increase this number or increase the frequency at which this script is run
|
||||
num_prs_to_search = 100
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_open_prs_for_repo(repo_id: str, num_prs: int):
|
||||
"""Return data about a specified number of open PRs for a specified repo
|
||||
|
||||
Arguments:
|
||||
repo_id: The node ID of the repo to search
|
||||
num_prs: The max number of PRs to return
|
||||
|
||||
Returns:
|
||||
Returns a JSON object of this structure:
|
||||
{
|
||||
"data": {
|
||||
"node": {
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": str,
|
||||
"isDraft": bool,
|
||||
"reviewRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"requestedReviewer": {
|
||||
"id": str
|
||||
}
|
||||
}...
|
||||
]
|
||||
},
|
||||
"projectCards": {
|
||||
"nodes": [
|
||||
{
|
||||
"project": {
|
||||
"id": str
|
||||
}
|
||||
}...
|
||||
]
|
||||
}
|
||||
}...
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
query = """query ($repo_id: ID!, $num_prs: Int!) {
|
||||
node(id: $repo_id) {
|
||||
... on Repository {
|
||||
pullRequests(last: $num_prs, states: OPEN) {
|
||||
nodes {
|
||||
id
|
||||
isDraft
|
||||
reviewRequests(first: 10) {
|
||||
nodes {
|
||||
requestedReviewer {
|
||||
... on Team {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
projectCards(first: 10) {
|
||||
nodes {
|
||||
project {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
variables = {
|
||||
"repo_id": github_repo_id,
|
||||
"num_prs": num_prs
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
json={'query': query, 'variables': variables},
|
||||
headers = {'Authorization': f"bearer {os.environ['TOKEN']}"}
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = json.loads(response.text)
|
||||
|
||||
if 'errors' in json_response:
|
||||
raise RuntimeError(f'Error in GraphQL response: {json_response}')
|
||||
|
||||
return json_response
|
||||
|
||||
def add_prs_to_board(prs_to_add: list, column_id: str):
|
||||
"""Adds PRs to a column of a project board
|
||||
|
||||
Arguments:
|
||||
prs_to_add: A list of PR node IDs
|
||||
column_id: The node ID of the column to add the PRs to
|
||||
|
||||
Returns:
|
||||
Nothing
|
||||
"""
|
||||
|
||||
logger.info(f"adding: {prs_to_add}")
|
||||
|
||||
mutation = """mutation($pr_id: ID!, $column_id: ID!) {
|
||||
addProjectCard(input:{contentId: $pr_id, projectColumnId: $column_id}) {
|
||||
projectColumn {
|
||||
name
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
for pr_id in prs_to_add:
|
||||
logger.info(f"Attempting to add {pr_id} to board")
|
||||
|
||||
variables = {
|
||||
"pr_id": pr_id,
|
||||
"column_id": column_id
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
json={'query': mutation, 'variables': variables},
|
||||
headers = {'Authorization': f"bearer {os.environ['TOKEN']}"}
|
||||
)
|
||||
|
||||
json_response = json.loads(response.text)
|
||||
|
||||
if 'errors' in json_response:
|
||||
logger.info(f"GraphQL error when adding {pr_id}: {json_response}")
|
||||
|
||||
def filter_prs(data, reviewer_id: str, project_id):
|
||||
"""Given data about the draft state, reviewers, and project boards for PRs,
|
||||
return just the PRs that are:
|
||||
- not draft
|
||||
- are requesting a review for the specified team
|
||||
- are not already on the specified project board
|
||||
|
||||
Arguments:
|
||||
data: A JSON object of this structure:
|
||||
{
|
||||
"data": {
|
||||
"node": {
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": str,
|
||||
"isDraft": bool,
|
||||
"reviewRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"requestedReviewer": {
|
||||
"id": str
|
||||
}
|
||||
}...
|
||||
]
|
||||
},
|
||||
"projectCards": {
|
||||
"nodes": [
|
||||
{
|
||||
"project": {
|
||||
"id": str
|
||||
}
|
||||
}...
|
||||
]
|
||||
}
|
||||
}...
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reviewer_id: The node ID of the reviewer to filter for
|
||||
project_id: The project ID of the project to filter against
|
||||
|
||||
Returns:
|
||||
A list of node IDs of the PRs that met the requirements
|
||||
"""
|
||||
|
||||
pr_data = data['data']['node']['pullRequests']['nodes']
|
||||
|
||||
prs_to_add = []
|
||||
|
||||
for pr in pr_data:
|
||||
if (
|
||||
not pr['isDraft'] and
|
||||
reviewer_id in [req_rev['requestedReviewer']['id'] for req_rev in pr['reviewRequests']['nodes'] if req_rev['requestedReviewer']] and
|
||||
project_id not in [proj_card['project']['id'] for proj_card in pr['projectCards']['nodes']]
|
||||
):
|
||||
prs_to_add.append(pr['id'])
|
||||
|
||||
return prs_to_add
|
||||
|
||||
def main():
|
||||
query_data = find_open_prs_for_repo(github_repo_id, num_prs_to_search)
|
||||
prs_to_add = filter_prs(query_data, docs_reviewers_id, docs_project_id)
|
||||
add_prs_to_board(prs_to_add, docs_column_id)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
.github/allowed-actions.js
vendored
@@ -9,6 +9,7 @@ module.exports = [
|
||||
"actions/github-script@626af12fe9a53dc2972b48385e7fe7dec79145c9", // v3.0.0
|
||||
"actions/labeler@5f867a63be70efff62b767459b009290364495eb", // v2.2.0
|
||||
"actions/setup-node@c46424eee26de4078d34105d3de3cc4992202b1e", // v2.1.4
|
||||
"actions/setup-python@dc73133d4da04e56a135ae2246682783cc7c7cb6", // v2.2.2
|
||||
"ruby/setup-ruby@fdcfbcf14ec9672f6f615cb9589a1bc5dd69d262", // v1.64.1
|
||||
"actions/stale@9d6f46564a515a9ea11e7762ab3957ee58ca50da", // v3.0.16
|
||||
"alex-page/github-project-automation-plus@fdb7991b72040d611e1123d2b75ff10eda9372c9",
|
||||
|
||||
34
.github/review-template.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
## Author self-review
|
||||
|
||||
- [ ] The changes in this PR meet the user experience and goals outlined in the content design plan.
|
||||
- [ ] I've compared my PR's source changes to staging and reviewed for versioning issues, redirects, the [style guide](https://github.com/github/docs/blob/main/contributing/content-style-guide.md), [content model](https://github.com/github/docs-content-strategy/blob/main/content-design/models.md), or [localization checklist](https://github.com/github/docs/blob/main/contributing/localization-checklist.md) rendering problems, typos, and wonky screenshots.
|
||||
- [ ] I've worked through build failures and tests are passing.
|
||||
- [ ] For REST API content, I've verified that endpoints, parameters, and responses are correct and work as expected and provided curl samples below.
|
||||
|
||||
For more information, check out our [full review guidelines and checklist](https://github.com/github/docs-content/blob/main/docs-content-docs/docs-content-workflows/reviews-and-feedback/review-process.md).
|
||||
|
||||
## Review request
|
||||
|
||||
### Summary
|
||||
|
||||
_Help reviewers understand this project and its context by writing a paragraph summarizing its goals and intended user experience and explaining how the PR meets those goals._
|
||||
[Content design plan](LINK HERE)
|
||||
|
||||
### Docs Content review
|
||||
|
||||
_Give Docs Content any extra context, highlight areas for them to consider in their review, and ask them questions you need answered to ship the PR._
|
||||
|
||||
### Technical review
|
||||
|
||||
_Ping in technical reviewers, asking them to review whether content is technically accurate and right for the audience._
|
||||
_Highlight areas for them to consider in their review and ask them questions you need answered to ship the PR._
|
||||
|
||||
### Content changes
|
||||
|
||||
[PR on staging](LINK HERE)
|
||||
|
||||
_Give a high-level overview of the changes in your PR and how they support the overall goals of the PR. Share links to important articles or changes in source and on staging. If your PR is large or complex, use a table to highlight changes with high user impact._
|
||||
|
||||
### Notes
|
||||
|
||||
_Discuss test failures, versioning issues, or anything else reviewers should know to consider the overall user experience of the PR._
|
||||
36
.github/workflows/add-review-template.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Add review template
|
||||
|
||||
# **What it does**: When a specific label is added to a PR, adds the contents of .github/review-template.md as a comment in the PR
|
||||
# **Why we have it**: To help Docs Content team members ensure that their PR is ready for review
|
||||
# **Who does it impact**: docs-internal maintainers and contributors
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
jobs:
|
||||
comment-that-approved:
|
||||
name: Add review template
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'docs-content-ready-for-review' && github.repository == 'github/docs-internal'
|
||||
|
||||
steps:
|
||||
- name: check out repo content
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
# Jump through some hoops to work with a multi-line file
|
||||
- name: Store review template in variable
|
||||
run: |
|
||||
TEMPLATE=$(cat .github/workflows/review-template.md)
|
||||
echo "TEMPLATE<<EOF" >> $GITHUB_ENV
|
||||
echo "$TEMPLATE" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Comment on the PR
|
||||
run: |
|
||||
gh pr comment $PR --body "$TEMPLATE"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.DOCUBOT_FR_PROJECT_BOARD_WORKFLOWS_REPO_ORG_READ_SCOPES}}
|
||||
PR: ${{ github.event.pull_request.html_url }}
|
||||
TEMPLATE: ${{ env.TEMPLATE }}
|
||||
35
.github/workflows/docs-review-collect.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Add docs-reviewers request to FR board
|
||||
|
||||
# **What it does**: Adds PRs in github/github that requested a review from docs-reviewers to the FR board
|
||||
# **Why we have it**: To catch docs-reviewers requests in github/github
|
||||
# **Who does it impact**: docs-content maintainers
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '25 4 * * *'
|
||||
|
||||
jobs:
|
||||
add-requests-to-board:
|
||||
name: Add requests to board
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repo content
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@dc73133d4da04e56a135ae2246682783cc7c7cb6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
- name: Run script
|
||||
run: |
|
||||
python .github/actions-scripts/fr-add-docs-reviewers-requests.py
|
||||
env:
|
||||
TOKEN: ${{ secrets.DOCS_BOT }}
|
||||
@@ -158,12 +158,12 @@ We (usually the docs team, but sometimes GitHub product managers, engineers, or
|
||||
You should always review your own PR first.
|
||||
|
||||
For content changes, make sure that you:
|
||||
- [ ] Confirm that the changes address every part of the content design plan from your issue (if there are differences, explain them).
|
||||
- [ ] Confirm that the changes meet the user experience and goals outlined in the content design plan (if there is one).
|
||||
- [ ] Compare your pull request's source changes to staging to confirm that the output matches the source and that everything is rendering as expected. This helps spot issues like typos, content that doesn't follow the style guide, or content that isn't rendering due to versioning problems. Remember that lists and tables can be tricky.
|
||||
- [ ] Review the content for technical accuracy.
|
||||
- [ ] Review the entire pull request using the [localization checklist](contributing/localization-checklist.md).
|
||||
- [ ] Copy-edit the changes for grammar, spelling, and adherence to the style guide.
|
||||
- [ ] Copy-edit the changes for grammar, spelling, and adherence to the [style guide](https://github.com/github/docs/blob/main/contributing/content-style-guide.md).
|
||||
- [ ] Check new or updated Liquid statements to confirm that versioning is correct.
|
||||
- [ ] Check that all of your changes render correctly in staging. Remember, that lists and tables can be tricky.
|
||||
- [ ] If there are any failing checks in your PR, troubleshoot them until they're all passing.
|
||||
|
||||
### Pull request template
|
||||
|
||||
BIN
assets/images/help/codespaces/add-dotnet-prebuilt-container.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/images/help/codespaces/add-dotnet-version.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
assets/images/help/codespaces/add-extension.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/images/help/codespaces/add-java-prebuilt-container.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/images/help/codespaces/add-java-version.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/help/codespaces/autofetch-all.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/images/help/codespaces/autofetch-search.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
BIN
assets/images/help/codespaces/branch-in-status-bar.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
BIN
assets/images/help/codespaces/codespace-overview-annotated.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/help/codespaces/codespaces-manage.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
assets/images/help/codespaces/codespaces-npm-run-dev.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/images/help/codespaces/codespaces-option-secrets-org.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
assets/images/help/codespaces/codespaces-option-secrets.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 73 KiB |
BIN
assets/images/help/codespaces/create-new-branch.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/images/help/codespaces/custom-prompt.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 46 KiB |
BIN
assets/images/help/codespaces/dotnet-extensions.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/images/help/codespaces/dotnet-options.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
assets/images/help/codespaces/fairyfloss.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
BIN
assets/images/help/codespaces/manage-button.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 109 KiB |
BIN
assets/images/help/codespaces/quickstart-port-toast.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/images/help/codespaces/secret-repository-access.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 68 KiB |
BIN
assets/images/help/codespaces/source-control-ellipsis-button.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 50 KiB |
BIN
assets/images/help/pull_requests/pull-request-body.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
assets/images/help/pull_requests/pull-request-comment.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
assets/images/help/repository/environments-top.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
BIN
assets/images/help/settings/codespaces-audit-log-org.png
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
assets/images/help/settings/codespaces-audit-log.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/images/help/sponsors/disable-your-account-button.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/images/help/sponsors/unpublish-profile-button.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/help/sponsors/unpublish-profile-dialog.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 36 KiB |
@@ -1,7 +1,7 @@
|
||||
import Head from 'next/head'
|
||||
|
||||
// import { Sidebar } from 'components/Sidebar'
|
||||
// import { Header } from 'components/Header'
|
||||
import { Header } from 'components/Header'
|
||||
import { SmallFooter } from 'components/SmallFooter'
|
||||
import { ScrollButton } from 'components/ScrollButton'
|
||||
import { SupportSection } from 'components/SupportSection'
|
||||
@@ -21,7 +21,7 @@ export const DefaultLayout = (props: Props) => {
|
||||
{/* <Sidebar /> */}
|
||||
|
||||
<main className="width-full">
|
||||
{/* <Header /> */}
|
||||
<Header />
|
||||
<DeprecationBanner />
|
||||
|
||||
{props.children}
|
||||
|
||||
113
components/Header.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import cx from 'classnames'
|
||||
import { css } from 'styled-components'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ChevronDownIcon, MarkGithubIcon, ThreeBarsIcon, XIcon } from '@primer/octicons-react'
|
||||
import { ButtonOutline } from '@primer/components'
|
||||
|
||||
import { useMainContext } from './context/MainContext'
|
||||
import { LanguagePicker } from './LanguagePicker'
|
||||
import { HeaderNotifications } from 'components/HeaderNotifications'
|
||||
import { MobileProductDropdown } from 'components/MobileProductDropdown'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
export const Header = () => {
|
||||
const router = useRouter()
|
||||
const { currentProduct, relativePath, error } = useMainContext()
|
||||
const { t } = useTranslation(['header', 'homepage'])
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="border-bottom color-border-secondary no-print">
|
||||
{error !== '404' && <HeaderNotifications />}
|
||||
|
||||
<header className="container-xl px-3 px-md-6 pt-3 pb-2 position-relative d-flex flex-justify-between width-full">
|
||||
<div
|
||||
className="d-flex flex-items-center d-lg-none"
|
||||
style={{ zIndex: 3 }}
|
||||
id="github-logo-mobile"
|
||||
role="banner"
|
||||
>
|
||||
<Link href={`/${router.locale}`}>
|
||||
<a aria-hidden="true" tabIndex={-1}>
|
||||
<MarkGithubIcon size={32} className="color-icon-primary" />
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Link href={`/${router.locale}`}>
|
||||
<a className="h4-mktg color-text-primary no-underline no-wrap pl-2">
|
||||
{t('github_docs')}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="width-full">
|
||||
<div className="d-inline-block width-full d-md-flex" style={{ zIndex: 1 }}>
|
||||
<div className="float-right d-md-none position-relative" style={{ zIndex: 3 }}>
|
||||
<ButtonOutline css onClick={() => setIsMenuOpen(!isMenuOpen)}>
|
||||
{isMenuOpen ? <XIcon size="small" /> : <ThreeBarsIcon size="small" />}
|
||||
</ButtonOutline>
|
||||
</div>
|
||||
<div
|
||||
style={{ zIndex: 2 }}
|
||||
className={cx('nav-mobile-dropdown width-full', isMenuOpen && 'js-open')}
|
||||
>
|
||||
<div className="d-md-flex flex-justify-between flex-items-center">
|
||||
<div className="py-2 py-md-0 d-md-inline-block">
|
||||
<h4 className="text-mono f5 text-normal color-text-secondary d-md-none">
|
||||
{t('explore_by_product')}
|
||||
</h4>
|
||||
<details className="dropdown-withArrow position-relative details details-reset d-md-none close-when-clicked-outside">
|
||||
<summary
|
||||
className="nav-desktop-productDropdownButton color-text-link py-2"
|
||||
role="button"
|
||||
aria-label="Toggle products list"
|
||||
>
|
||||
<div
|
||||
id="current-product"
|
||||
className="d-flex flex-items-center flex-justify-between"
|
||||
style={{ paddingTop: 2 }}
|
||||
>
|
||||
{/* <!-- Product switcher - GitHub.com, Enterprise Server, etc -->
|
||||
<!-- 404 and 500 error layouts are not real pages so we need to hardcode the name for those --> */}
|
||||
{currentProduct.name}
|
||||
<ChevronDownIcon size={24} className="arrow ml-md-1" />
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<MobileProductDropdown />
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{/* <!-- Versions picker that only appears in the header on landing pages --> */}
|
||||
{/* {% include header-version-switcher %} */}
|
||||
|
||||
<div className="d-md-inline-block">
|
||||
{/* <!-- Language picker - 'English', 'Japanese', etc --> */}
|
||||
<div className="border-top border-md-top-0 py-2 py-md-0 d-md-inline-block">
|
||||
<LanguagePicker />
|
||||
</div>
|
||||
|
||||
{/* <!-- GitHub.com homepage and 404 page has a stylized search; Enterprise homepages do not --> */}
|
||||
{relativePath !== 'index.md' && error !== '404'}
|
||||
<div
|
||||
className="pt-3 pt-md-0 d-md-inline-block ml-md-3 border-top border-md-top-0"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
<div id="search-input-container" aria-hidden="true"></div>
|
||||
<div id="search-results-container"></div>
|
||||
<div class="search-overlay-desktop"></div>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
89
components/HeaderNotifications.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import cx from 'classnames'
|
||||
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { ExcludesNull } from 'components/lib/ExcludesNull'
|
||||
import { useVersion } from './hooks/useVersion'
|
||||
|
||||
enum NotificationType {
|
||||
RELEASE = 'RELEASE',
|
||||
TRANSLATION = 'TRANSLATION',
|
||||
EARLY_ACCESS = 'EARLY_ACCESS',
|
||||
}
|
||||
|
||||
type Notif = {
|
||||
content: string
|
||||
type: NotificationType
|
||||
}
|
||||
export const HeaderNotifications = () => {
|
||||
const router = useRouter()
|
||||
const { currentVersion } = useVersion()
|
||||
const { relativePath, allVersions, data, languages, currentLanguage } = useMainContext()
|
||||
const { t } = useTranslation('header')
|
||||
|
||||
const translationNotices: Array<Notif> = []
|
||||
if (router.locale !== 'en') {
|
||||
if (relativePath?.includes('/site-policy')) {
|
||||
translationNotices.push({
|
||||
type: NotificationType.TRANSLATION,
|
||||
content: data.reusables.policies.translation,
|
||||
})
|
||||
} else if (languages[currentLanguage].wip !== true) {
|
||||
translationNotices.push({
|
||||
type: NotificationType.TRANSLATION,
|
||||
content: t('notices.localization_complete'),
|
||||
})
|
||||
} else if (languages[currentLanguage].wip) {
|
||||
translationNotices.push({
|
||||
type: NotificationType.TRANSLATION,
|
||||
content: t('notices.localization_in_progress'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const releaseNotices: Array<Notif> = []
|
||||
if (currentVersion === 'github-ae@latest') {
|
||||
releaseNotices.push({
|
||||
type: NotificationType.RELEASE,
|
||||
content: t('notices.ghae_silent_launch'),
|
||||
})
|
||||
} else if (currentVersion === data.variables.release_candidate.version) {
|
||||
releaseNotices.push({
|
||||
type: NotificationType.RELEASE,
|
||||
content: `${allVersions[currentVersion].versionTitle}${t('notices.release_candidate')}`,
|
||||
})
|
||||
}
|
||||
|
||||
const allNotifications: Array<Notif> = [
|
||||
...translationNotices,
|
||||
...releaseNotices,
|
||||
// ONEOFF EARLY ACCESS NOTICE
|
||||
(relativePath || '').includes('early-access/')
|
||||
? {
|
||||
type: NotificationType.EARLY_ACCESS,
|
||||
content: t('notices.early_access'),
|
||||
}
|
||||
: null,
|
||||
].filter(ExcludesNull)
|
||||
|
||||
return (
|
||||
<>
|
||||
{allNotifications.map(({ type, content }, i) => {
|
||||
const isLast = i !== allNotifications.length - 1
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'header-notifications text-center f5 color-text-primary py-4 px-6',
|
||||
type === NotificationType.TRANSLATION && 'translation_notice color-bg-info',
|
||||
type === NotificationType.RELEASE && 'release_notice color-bg-info',
|
||||
type === NotificationType.EARLY_ACCESS && 'early_access color-bg-danger',
|
||||
!isLast && 'border-bottom color-border-tertiary'
|
||||
)}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
42
components/LanguagePicker.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Dropdown } from '@primer/components'
|
||||
import { useMainContext } from './context/MainContext'
|
||||
|
||||
export const LanguagePicker = () => {
|
||||
const router = useRouter()
|
||||
const { languages } = useMainContext()
|
||||
const locale = router.locale || 'en'
|
||||
const langs = Object.values(languages)
|
||||
const selectedLang = languages[locale]
|
||||
|
||||
return (
|
||||
<div className="ml-4 d-flex flex-justify-center flex-items-center">
|
||||
<Dropdown css>
|
||||
<summary>
|
||||
{selectedLang.nativeName || selectedLang.name}
|
||||
<Dropdown.Caret />
|
||||
</summary>
|
||||
<Dropdown.Menu direction="sw">
|
||||
{langs.map((lang) => {
|
||||
return (
|
||||
<Dropdown.Item key={lang.code}>
|
||||
<Link href={router.asPath} locale={lang.hreflang}>
|
||||
<a>
|
||||
{lang.nativeName ? (
|
||||
<>
|
||||
{lang.nativeName} ({lang.name})
|
||||
</>
|
||||
) : (
|
||||
lang.name
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
</Dropdown.Item>
|
||||
)
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
44
components/MobileProductDropdown.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LinkExternalIcon } from '@primer/octicons-react'
|
||||
import cx from 'classnames'
|
||||
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
|
||||
export const MobileProductDropdown = () => {
|
||||
const router = useRouter()
|
||||
const { activeProducts, currentProduct } = useMainContext()
|
||||
|
||||
return (
|
||||
<div
|
||||
id="homepages"
|
||||
className="position-md-absolute nav-desktop-productDropdown p-md-4 left-md-n4 top-md-6"
|
||||
style={{ zIndex: 6 }}
|
||||
>
|
||||
{activeProducts.map((product) => {
|
||||
return (
|
||||
<Link
|
||||
key={product.id}
|
||||
href={`${product.external ? '' : `/${router.locale}`}${product.href}`}
|
||||
>
|
||||
<a
|
||||
className={cx(
|
||||
'd-block py-2',
|
||||
product.id === currentProduct.id
|
||||
? 'color-text-link text-underline active'
|
||||
: 'Link--primary no-underline'
|
||||
)}
|
||||
>
|
||||
{product.name}
|
||||
{product.external && (
|
||||
<span className="ml-1">
|
||||
<LinkExternalIcon size="small" />
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
components/TruncateLines.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { ReactNode, ReactHTML } from 'react'
|
||||
import cx from 'classnames'
|
||||
|
||||
type Props = {
|
||||
as?: keyof ReactHTML
|
||||
maxLines: number
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
export const TruncateLines = (props: Props) => {
|
||||
const Component = props.as || 'div'
|
||||
return (
|
||||
<Component className={cx('root', props.className)}>
|
||||
{props.children}
|
||||
<style jsx>{`
|
||||
.root {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${props.maxLines};
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
`}</style>
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
44
components/VersionPicker.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Dropdown } from '@primer/components'
|
||||
import { useMainContext } from './context/MainContext'
|
||||
import { useVersion } from './hooks/useVersion'
|
||||
|
||||
export const VersionPicker = () => {
|
||||
const router = useRouter()
|
||||
const { currentVersion } = useVersion()
|
||||
const { allVersions } = useMainContext()
|
||||
|
||||
const versions = Object.values(allVersions)
|
||||
const activeVersion = allVersions[currentVersion]
|
||||
|
||||
return (
|
||||
<div className="ml-4 d-flex flex-justify-center flex-items-center">
|
||||
<Dropdown css>
|
||||
<summary>
|
||||
{activeVersion.versionTitle}
|
||||
<Dropdown.Caret />
|
||||
</summary>
|
||||
<Dropdown.Menu direction="sw">
|
||||
{versions.map((version) => {
|
||||
return (
|
||||
<Dropdown.Item key={version.version}>
|
||||
<Link
|
||||
href={{
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
versionId: version.version,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<a>{version.versionTitle}</a>
|
||||
</Link>
|
||||
</Dropdown.Item>
|
||||
)
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,19 @@ type ProductT = {
|
||||
name: string
|
||||
}
|
||||
|
||||
type LanguageItem = {
|
||||
name: string
|
||||
nativeName: string
|
||||
code: string
|
||||
hreflang: string
|
||||
wip?: boolean
|
||||
}
|
||||
|
||||
type VersionItem = {
|
||||
version: string
|
||||
versionTitle: string
|
||||
}
|
||||
|
||||
type DataT = {
|
||||
ui: Record<string, any>
|
||||
reusables: {
|
||||
@@ -18,6 +31,12 @@ type DataT = {
|
||||
deprecation_details: string
|
||||
isOldestReleaseDeprecated: boolean
|
||||
}
|
||||
policies: {
|
||||
translation: string
|
||||
}
|
||||
}
|
||||
variables: {
|
||||
release_candidate: { version: string }
|
||||
}
|
||||
}
|
||||
type EnterpriseServerReleases = {
|
||||
@@ -34,9 +53,13 @@ export type MainContextT = {
|
||||
currentLayoutName: string
|
||||
data: DataT
|
||||
airGap?: boolean
|
||||
error: string
|
||||
currentCategory?: string
|
||||
relativePath?: string
|
||||
enterpriseServerReleases: EnterpriseServerReleases
|
||||
currentLanguage: string
|
||||
languages: Record<string, LanguageItem>
|
||||
allVersions: Record<string, VersionItem>
|
||||
}
|
||||
|
||||
export const getMainContextFromRequest = (req: any): MainContextT => {
|
||||
@@ -47,16 +70,36 @@ export const getMainContextFromRequest = (req: any): MainContextT => {
|
||||
activeProducts: req.context.activeProducts,
|
||||
currentProduct: req.context.productMap[req.context.currentProduct],
|
||||
currentLayoutName: req.context.currentLayoutName,
|
||||
error: req.context.error || '',
|
||||
data: {
|
||||
ui: req.context.site.data.ui,
|
||||
reusables: {
|
||||
enterprise_deprecation: req.context.site.data.reusables.enterprise_deprecation,
|
||||
policies: req.context.site.data.reusables.policies,
|
||||
},
|
||||
variables: {
|
||||
release_candidate: req.context.site.data.variables.release_candidate,
|
||||
},
|
||||
},
|
||||
airGap: req.context.AIRGAP || false,
|
||||
currentCategory: req.context.currentCategory || '',
|
||||
relativePath: req.context.page.relativePath,
|
||||
enterpriseServerReleases: req.context.enterpriseServerReleases,
|
||||
currentLanguage: req.context.currentLanguage,
|
||||
languages: Object.fromEntries(
|
||||
Object.entries(req.context.languages).map(([key, entry]: any) => {
|
||||
return [
|
||||
key,
|
||||
{
|
||||
name: entry.name,
|
||||
nativeName: entry.nativeName || '',
|
||||
code: entry.code,
|
||||
hreflang: entry.hreflang,
|
||||
},
|
||||
]
|
||||
})
|
||||
),
|
||||
allVersions: req.context.allVersions,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
126
components/context/ProductLandingContext.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import pick from 'lodash/pick'
|
||||
|
||||
export type FeaturedLink = {
|
||||
title: string
|
||||
href: string
|
||||
intro?: string
|
||||
authors?: Array<string>
|
||||
hideIntro?: boolean
|
||||
date?: string
|
||||
}
|
||||
export type CodeExample = {
|
||||
title: string
|
||||
description: string
|
||||
languages: string // single comma separated string
|
||||
href: string
|
||||
tags: Array<string>
|
||||
}
|
||||
|
||||
export type ProductLandingContextT = {
|
||||
title: string
|
||||
introPlainText: string
|
||||
shortTitle: string
|
||||
intro: string
|
||||
beta_product: boolean
|
||||
// primaryAction: LinkButtonT
|
||||
// secondaryAction?: LinkButtonT
|
||||
introLinks: {
|
||||
quickstart?: string
|
||||
reference?: string
|
||||
overview?: string
|
||||
}
|
||||
product_video?: string
|
||||
// featuredLinks?: {
|
||||
// guides: Array<FeaturedLink>
|
||||
// popular: Array<FeaturedLink>
|
||||
// guideCards: Array<FeaturedLink>
|
||||
// }
|
||||
guideCards: Array<FeaturedLink>
|
||||
productCodeExamples: Array<CodeExample>
|
||||
productUserExamples: Array<{ username: string; description: string }>
|
||||
productCommunityExamples: Array<{ repo: string; description: string }>
|
||||
featuredArticles: Array<{
|
||||
label: string // Guides
|
||||
viewAllHref?: string // If provided, adds a "View All ->" to the header
|
||||
articles: Array<FeaturedLink>
|
||||
}>
|
||||
changelog: { label: string; prefix: string }
|
||||
changelogUrl?: string
|
||||
whatsNewChangelog?: Array<{ href: string; title: string; date: string }>
|
||||
}
|
||||
|
||||
export const ProductLandingContext = createContext<ProductLandingContextT | null>(null)
|
||||
|
||||
export const useProductLandingContext = (): ProductLandingContextT => {
|
||||
const context = useContext(ProductLandingContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'"useProductLandingContext" may only be used inside "ProductLandingContext.Provider"'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const getProductLandingContextFromRequest = (req: any): ProductLandingContextT => {
|
||||
const { currentCategory, currentPath, data } = req.context
|
||||
return {
|
||||
...pick(req.context.page, [
|
||||
'title',
|
||||
'shortTitle',
|
||||
'introPlainText',
|
||||
'beta_product',
|
||||
'intro',
|
||||
'product_video',
|
||||
'changelog',
|
||||
]),
|
||||
whatsNewChangelog: req.context.whatsNewChangelog,
|
||||
changelogUrl: req.context.changelogUrl,
|
||||
|
||||
productCodeExamples: req.context.productCodeExamples || [],
|
||||
|
||||
productCommunityExamples: req.context.productCommunityExamples || [],
|
||||
|
||||
productUserExamples: (req.context.productUserExamples || []).map(
|
||||
({ user, description }: any) => ({
|
||||
username: user,
|
||||
description,
|
||||
})
|
||||
),
|
||||
|
||||
introLinks: Object.fromEntries(
|
||||
Object.entries(req.context.page.introLinks || {}).filter(([key, val]) => !!val)
|
||||
),
|
||||
|
||||
guideCards: (req.context.featuredLinks.guideCards || []).map((link: any) => {
|
||||
return {
|
||||
href: link.href,
|
||||
title: link.title,
|
||||
intro: link.intro,
|
||||
authors: link.page.authors || [],
|
||||
}
|
||||
}),
|
||||
|
||||
featuredArticles: Object.entries(req.context.featuredLinks)
|
||||
.filter(([key]) => {
|
||||
return key === 'guides' || key === 'popular'
|
||||
})
|
||||
.map(([key, links]: any) => {
|
||||
return {
|
||||
label: req.context.site.data.ui.toc[key],
|
||||
viewAllHref: key === 'guides' && !currentCategory ? `${currentPath}/${key}` : '',
|
||||
articles: links.map((link: any) => {
|
||||
return {
|
||||
hideIntro: key === 'popular',
|
||||
href: link.href,
|
||||
title: link.title,
|
||||
intro: link.intro,
|
||||
authors: link.page.authors || [],
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,39 @@ import get from 'lodash/get'
|
||||
|
||||
// The idea of this component is to mimic a popular i18n library (i18next)
|
||||
// so that we can set ourselves up to transition to it (or a similar library) in the future
|
||||
export const useTranslation = (translationGroup: string) => {
|
||||
export const useTranslation = (namespaces: string | Array<string>) => {
|
||||
const { data } = useMainContext()
|
||||
|
||||
// this can eventually be an object constructed from the input namespaces param above, but for now everything is already loaded
|
||||
const loadedData: any = data.ui
|
||||
|
||||
return {
|
||||
// The compiled string supports prefixing with a namespace such as `my-namespace:path.to.value`
|
||||
t: (strings: TemplateStringsArray | string, ...values: Array<any>) => {
|
||||
const key = typeof strings === 'string' ? strings : String.raw(strings, ...values)
|
||||
return get((data.ui as any)[translationGroup], key)
|
||||
|
||||
const splitKey = key.split(':')
|
||||
if (splitKey.length > 2) {
|
||||
throw new Error('Multiple ":" not allowed in translation lookup path')
|
||||
}
|
||||
|
||||
if (splitKey.length === 2) {
|
||||
const [namespace, path] = splitKey
|
||||
return get(loadedData[namespace], path)
|
||||
}
|
||||
|
||||
const [path] = splitKey
|
||||
if (Array.isArray(namespaces)) {
|
||||
for (const namespace of namespaces) {
|
||||
const val = get(loadedData[namespace], path)
|
||||
if (val !== undefined) {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
} else {
|
||||
return get(loadedData[namespaces], path)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
35
components/landing/CodeExampleCard.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { RepoIcon } from '@primer/octicons-react'
|
||||
import { CodeExample } from 'components/context/ProductLandingContext'
|
||||
|
||||
type Props = {
|
||||
example: CodeExample
|
||||
}
|
||||
export const CodeExampleCard = ({ example }: Props) => {
|
||||
return (
|
||||
<a
|
||||
className="Box d-flex flex-column flex-justify-between height-full color-shadow-medium hover-shadow-large no-underline color-text-primary"
|
||||
href={`https://github.com/${example.href}`}
|
||||
>
|
||||
<div className="p-4">
|
||||
<h4>{example.title}</h4>
|
||||
<p className="mt-2 mb-4 color-text-tertiary">{example.description}</p>
|
||||
<div className="d-flex flex-wrap">
|
||||
{example.tags.map((tag) => {
|
||||
return (
|
||||
<span
|
||||
key={tag}
|
||||
className="IssueLabel color-text-inverse color-bg-info-inverse mr-2 mb-1"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<footer className="border-top p-4 color-text-secondary d-flex flex-items-center">
|
||||
<RepoIcon className="flex-shrink-0" />
|
||||
<span className="ml-2 text-mono text-small color-text-link">{example.href}</span>
|
||||
</footer>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
80
components/landing/CodeExamples.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useState } from 'react'
|
||||
import { ArrowRightIcon, SearchIcon } from '@primer/octicons-react'
|
||||
|
||||
import { useProductLandingContext } from 'components/context/ProductLandingContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { CodeExampleCard } from 'components/landing/CodeExampleCard'
|
||||
|
||||
const PAGE_SIZE = 6
|
||||
export const CodeExamples = () => {
|
||||
const { productCodeExamples } = useProductLandingContext()
|
||||
const { t } = useTranslation('product_landing')
|
||||
const [numVisible, setNumVisible] = useState(PAGE_SIZE)
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const onSearchChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearch(e.target.value)
|
||||
setNumVisible(PAGE_SIZE) // reset the visible count (only matters after searching)
|
||||
}
|
||||
|
||||
const isSearching = !!search
|
||||
let searchResults: typeof productCodeExamples = []
|
||||
if (isSearching) {
|
||||
const matchReg = new RegExp(search, 'i')
|
||||
searchResults = productCodeExamples.filter((example) => {
|
||||
const searchableStr = `${example.tags.join(' ')} ${example.title} ${example.description}`
|
||||
return matchReg.test(searchableStr)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pr-lg-3 mb-5 mt-3">
|
||||
<input
|
||||
className="input-lg py-2 px-3 col-12 col-lg-8 form-control"
|
||||
placeholder={t('search_code_examples')}
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
aria-label={t('search_code_examples')}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-wrap gutter">
|
||||
{(isSearching ? searchResults : productCodeExamples.slice(0, numVisible)).map((example) => {
|
||||
return (
|
||||
<div key={example.href} className="col-12 col-xl-4 col-lg-6 mb-4">
|
||||
<CodeExampleCard example={example} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{numVisible < productCodeExamples.length && !isSearching && (
|
||||
<button
|
||||
className="btn btn-outline float-right"
|
||||
onClick={() => setNumVisible(numVisible + PAGE_SIZE)}
|
||||
>
|
||||
{t('show_more')} <ArrowRightIcon />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isSearching && searchResults.length === 0 && (
|
||||
<div className="d-none py-4 text-center color-text-secondary font-mktg">
|
||||
<div className="mb-3">
|
||||
<SearchIcon size={24} />{' '}
|
||||
</div>
|
||||
<h3 className="text-normal">
|
||||
{t('sorry')} <strong className="js-filter-card-value"></strong>
|
||||
</h3>
|
||||
<p className="my-3 f4">
|
||||
{t('no_example')} <br /> {t('try_another')}
|
||||
</p>
|
||||
<a href="https://github.com/github/docs/blob/main/data/variables/discussions_community_examples.yml">
|
||||
{t('add_your_community')} <ArrowRightIcon />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
components/landing/CommunityExamples.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useState } from 'react'
|
||||
import { ArrowRightIcon } from '@primer/octicons-react'
|
||||
|
||||
import { useProductLandingContext } from 'components/context/ProductLandingContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { RepoCard } from 'components/landing/RepoCard'
|
||||
|
||||
export const CommunityExamples = () => {
|
||||
const { productCommunityExamples } = useProductLandingContext()
|
||||
const { t } = useTranslation('product_landing')
|
||||
const [numVisible, setNumVisible] = useState(6)
|
||||
|
||||
if (!productCommunityExamples) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex flex-wrap gutter">
|
||||
{productCommunityExamples.slice(0, numVisible).map((repo, i) => {
|
||||
return (
|
||||
<div key={repo.repo} className="col-12 col-xl-4 col-lg-6 mb-4">
|
||||
<RepoCard repo={repo} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{numVisible < productCommunityExamples.length && (
|
||||
<button
|
||||
className="btn btn-outline float-right"
|
||||
onClick={() => setNumVisible(productCommunityExamples.length)}
|
||||
>
|
||||
{t('show_more')} <ArrowRightIcon />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
109
components/landing/FeaturedArticles.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import cx from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
import { ArrowRightIcon } from '@primer/octicons-react'
|
||||
import { FeaturedLink, useProductLandingContext } from 'components/context/ProductLandingContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { TruncateLines } from 'components/TruncateLines'
|
||||
|
||||
export const FeaturedArticles = () => {
|
||||
const {
|
||||
featuredArticles = [],
|
||||
changelog,
|
||||
whatsNewChangelog,
|
||||
changelogUrl,
|
||||
} = useProductLandingContext()
|
||||
const { t } = useTranslation('toc')
|
||||
|
||||
return (
|
||||
<div className="d-lg-flex gutter my-6 py-6">
|
||||
{featuredArticles.map((section, i) => {
|
||||
return (
|
||||
<div
|
||||
key={section.label}
|
||||
className={cx('col-12 mb-4 mb-lg-0', changelog ? 'col-lg-4' : 'col-lg-6')}
|
||||
>
|
||||
<ArticleList
|
||||
title={section.label}
|
||||
viewAllHref={section.viewAllHref}
|
||||
articles={section.articles}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{changelog && (
|
||||
<div className={cx('col-12 mb-4 mb-lg-0', changelog ? 'col-lg-4' : 'col-lg-6')}>
|
||||
<ArticleList
|
||||
title={t('whats_new')}
|
||||
viewAllHref={changelogUrl}
|
||||
articles={(whatsNewChangelog || []).map((link) => {
|
||||
return {
|
||||
title: link.title,
|
||||
date: link.date,
|
||||
href: link.href,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ArticleListProps = {
|
||||
title: string
|
||||
viewAllHref?: string
|
||||
articles: Array<FeaturedLink>
|
||||
}
|
||||
const ArticleList = ({ title, viewAllHref, articles }: ArticleListProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="featured-links-heading mb-4 d-flex flex-items-baseline">
|
||||
<h3 className="f4 text-normal text-mono text-uppercase color-text-secondary">{title}</h3>
|
||||
{viewAllHref && (
|
||||
<Link href={viewAllHref}>
|
||||
<a className="ml-4">
|
||||
View all <ArrowRightIcon size={14} className="v-align-middle" />
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="list-style-none">
|
||||
{articles.map((link) => {
|
||||
return (
|
||||
<li key={link.href} className="border-top">
|
||||
<Link href={link.href}>
|
||||
<a className="link-with-intro Bump-link--hover no-underline d-block py-3">
|
||||
<h4 className="link-with-intro-title">
|
||||
{link.title}
|
||||
<span className="Bump-link-symbol">→</span>
|
||||
</h4>
|
||||
{!link.hideIntro && link.intro && (
|
||||
<TruncateLines
|
||||
as="p"
|
||||
maxLines={2}
|
||||
className="link-with-intro-intro color-text-secondary mb-0 mt-1"
|
||||
>
|
||||
{link.intro}
|
||||
</TruncateLines>
|
||||
)}
|
||||
{link.date && (
|
||||
<time
|
||||
className="tooltipped tooltipped-n color-text-tertiary text-mono mt-1"
|
||||
aria-label={format(new Date(link.date), 'PPP')}
|
||||
>
|
||||
{format(new Date(link.date), 'MMMM dd')}
|
||||
</time>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
52
components/landing/GuideCard.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { FeaturedLink } from 'components/context/ProductLandingContext'
|
||||
|
||||
type Props = {
|
||||
guide: FeaturedLink
|
||||
}
|
||||
export const GuideCard = ({ guide }: Props) => {
|
||||
const authors = guide.authors && guide.authors.length > 0 ? guide.authors : ['GitHub']
|
||||
const authorString = `@${authors.join(', @')}`
|
||||
|
||||
return (
|
||||
<div className="col-lg-4 col-12 mb-3">
|
||||
<a
|
||||
className="Box color-shadow-medium height-full d-block hover-shadow-large no-underline color-text-primary p-5"
|
||||
href={guide.href}
|
||||
>
|
||||
<h2>{guide.title}</h2>
|
||||
<p className="mt-2 mb-4 color-text-tertiary">{guide.intro}</p>
|
||||
|
||||
<footer className="d-flex">
|
||||
<div className="mr-1">
|
||||
{authors.length === 1 ? (
|
||||
<img
|
||||
className="avatar avatar-2 circle mr-1"
|
||||
src={`https://github.com/${authors[0]}.png`}
|
||||
alt={`@${authors[0]}`}
|
||||
/>
|
||||
) : (
|
||||
<div className="AvatarStack AvatarStack--three-plus">
|
||||
<div
|
||||
className="AvatarStack-body tooltipped tooltipped-se tooltipped-align-left-1"
|
||||
aria-label={authorString}
|
||||
>
|
||||
{authors.map((author) => {
|
||||
return (
|
||||
<img
|
||||
className="avatar circle"
|
||||
alt={`@${author}`}
|
||||
src={`https://github.com/${author}.png`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>{authorString}</div>
|
||||
</footer>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
components/landing/GuideCards.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { ArrowRightIcon } from '@primer/octicons-react'
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
|
||||
import { useProductLandingContext } from 'components/context/ProductLandingContext'
|
||||
import { GuideCard } from 'components/landing/GuideCard'
|
||||
|
||||
export const GuideCards = () => {
|
||||
const router = useRouter()
|
||||
const { currentCategory } = useMainContext()
|
||||
const { guideCards } = useProductLandingContext()
|
||||
|
||||
if (!guideCards) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-lg-flex gutter-lg flex-items-stretch">
|
||||
{(guideCards || []).map((guide) => {
|
||||
return <GuideCard key={guide.href} guide={guide} />
|
||||
})}
|
||||
</div>
|
||||
|
||||
{!currentCategory && (
|
||||
<Link href={`${router.asPath}/guides`}>
|
||||
<a className="btn btn-outline float-right">
|
||||
Explore guides <ArrowRightIcon />
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
107
components/landing/LandingHero.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import cx from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
|
||||
import { useProductLandingContext } from 'components/context/ProductLandingContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { useVersion } from 'components/hooks/useVersion'
|
||||
|
||||
export const LandingHero = () => {
|
||||
const { airGap } = useMainContext()
|
||||
const { product_video, shortTitle, beta_product, intro, introLinks } = useProductLandingContext()
|
||||
const { t } = useTranslation('product_landing')
|
||||
|
||||
return (
|
||||
<header className="d-lg-flex gutter-lg mb-6">
|
||||
<div className={cx(product_video && 'col-12 col-lg-6 mb-3 mb-lg-0')}>
|
||||
<span className="text-mono color-text-secondary">Product</span>
|
||||
<h1 className="mb-3 font-mktg">
|
||||
{shortTitle}{' '}
|
||||
{beta_product && <span className="Label Label--success v-align-middle">Beta</span>}
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className="lead-mktg color-text-secondary"
|
||||
dangerouslySetInnerHTML={{ __html: intro }}
|
||||
/>
|
||||
|
||||
{/* idea to abstract the introLinks into something more component-like */}
|
||||
{/* {introLinks.map((link) => {
|
||||
return (
|
||||
<FullLink
|
||||
href={link.href}
|
||||
className={cx(
|
||||
'btn-mktg btn-large f4 mt-3 mr-3',
|
||||
link.secondary && 'btn-outline-mktg'
|
||||
)}
|
||||
>
|
||||
{t(link.translationKeyLabel)}
|
||||
</FullLink>
|
||||
)
|
||||
})} */}
|
||||
|
||||
{introLinks?.quickstart && (
|
||||
<FullLink href={introLinks.quickstart} className="btn-mktg btn-large f4 mt-3 mr-3">
|
||||
{t('quickstart')}
|
||||
</FullLink>
|
||||
)}
|
||||
|
||||
{introLinks?.reference && (
|
||||
<FullLink
|
||||
href={introLinks.reference}
|
||||
className="btn-mktg btn-outline-mktg btn-large f4 mt-3 mr-3"
|
||||
>
|
||||
{t('reference')}
|
||||
</FullLink>
|
||||
)}
|
||||
|
||||
{introLinks?.overview && (
|
||||
<FullLink
|
||||
href={introLinks.overview}
|
||||
className="btn-mktg btn-outline-mktg btn-large f4 mt-3 mr-3"
|
||||
>
|
||||
{t('overview')}
|
||||
</FullLink>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{product_video && (
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="position-relative" style={{ paddingBottom: '56.25%' }}>
|
||||
{!airGap && (
|
||||
<iframe
|
||||
title={`${shortTitle} Video`}
|
||||
className="top-0 left-0 position-absolute color-shadow-large rounded-1 width-full height-full"
|
||||
src={product_video}
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
// Fully Qualified Link - it includes the version and locale in the path
|
||||
type Props = {
|
||||
href: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
export const FullLink = ({ href, children, className }: Props) => {
|
||||
const router = useRouter()
|
||||
const { currentVersion } = useVersion()
|
||||
const locale = router.locale || 'en'
|
||||
const fullyQualifiedHref = `/${locale}${
|
||||
currentVersion !== 'free-pro-team@latest' ? `/${currentVersion}` : ''
|
||||
}${href}`
|
||||
return (
|
||||
<Link href={fullyQualifiedHref}>
|
||||
<a className={className}>{children}</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
15
components/landing/LandingSection.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import cx from 'classnames'
|
||||
|
||||
type Props = {
|
||||
title?: string
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
export const LandingSection = ({ title, children, className }: Props) => {
|
||||
return (
|
||||
<div className={cx('container-xl px-3 px-md-6', className)}>
|
||||
{title && <h2 className="font-mktg h1 mb-2">{title}</h2>}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
components/landing/RepoCard.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
type Props = {
|
||||
repo: {
|
||||
repo: string
|
||||
description: string
|
||||
}
|
||||
href?: string
|
||||
}
|
||||
export const RepoCard = ({ repo, href }: Props) => {
|
||||
return (
|
||||
<a
|
||||
className="Box d-flex height-full color-shadow-medium hover-shadow-large no-underline color-text-primary p-4"
|
||||
href={href || `https://github.com/${repo.repo}`}
|
||||
>
|
||||
<div className="flex-shrink-0 mr-3">
|
||||
<img
|
||||
src={`https://github.com/${repo.repo.split('/')[0]}.png`}
|
||||
alt={repo.repo}
|
||||
className="avatar avatar-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-auto">
|
||||
<h4>{repo.repo}</h4>
|
||||
<p className="mt-1 color-text-tertiary">{repo.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
34
components/landing/SponsorsExamples.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import Link from 'next/link'
|
||||
import { ArrowRightIcon } from '@primer/octicons-react'
|
||||
|
||||
import { useProductLandingContext } from 'components/context/ProductLandingContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { UserCard } from 'components/landing/UserCard'
|
||||
|
||||
export const SponsorsExamples = () => {
|
||||
const { productUserExamples } = useProductLandingContext()
|
||||
const { t } = useTranslation('product_landing')
|
||||
|
||||
if (!productUserExamples) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex flex-wrap gutter">
|
||||
{productUserExamples.slice(0, 6).map((user) => {
|
||||
return (
|
||||
<div key={user.username} className="col-12 col-xl-4 col-lg-6 mb-4">
|
||||
<UserCard href={`https://github.com/sponsors/${user.username}`} user={user} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<Link href={`https://github.com/sponsors/community`}>
|
||||
<a className="btn btn-outline float-right">
|
||||
{t('explore_people_and_projects')} <ArrowRightIcon />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
components/landing/UserCard.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
type Props = {
|
||||
user: {
|
||||
username: string
|
||||
description: string
|
||||
}
|
||||
href?: string
|
||||
}
|
||||
export const UserCard = ({ user, href }: Props) => {
|
||||
return (
|
||||
<a
|
||||
className="Box d-flex height-full color-shadow-medium hover-shadow-large no-underline color-text-primary p-4"
|
||||
href={href || `https://github.com/${user.username}`}
|
||||
>
|
||||
<div className="flex-shrink-0 mr-3">
|
||||
<img
|
||||
src={`https://github.com/${user.username}.png`}
|
||||
alt={user.username}
|
||||
className="avatar avatar-8 circle"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-auto">
|
||||
<h4>{user.username}</h4>
|
||||
<p className="mt-1 color-text-tertiary">{user.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
3
components/lib/ExcludesNull.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function ExcludesNull<T>(x: T | null): x is T {
|
||||
return x !== null
|
||||
}
|
||||
@@ -172,7 +172,7 @@ In this example, `cleanup.js` only runs on Linux-based runners:
|
||||
|
||||
```yaml
|
||||
pre: 'cleanup.js'
|
||||
pre-if: 'runner.os == linux'
|
||||
pre-if: runner.os == 'linux'
|
||||
```
|
||||
|
||||
#### `post`
|
||||
@@ -198,7 +198,7 @@ For example, this `cleanup.js` will only run on Linux-based runners:
|
||||
|
||||
```yaml
|
||||
post: 'cleanup.js'
|
||||
post-if: 'runner.os == linux'
|
||||
post-if: runner.os == 'linux'
|
||||
```
|
||||
|
||||
### `runs` for composite run steps actions
|
||||
|
||||
@@ -59,7 +59,16 @@ For more information, see [`actions/cache`](https://github.com/actions/cache).
|
||||
|
||||
- `key`: **Required** The key created when saving a cache and the key used to search for a cache. Can be any combination of variables, context values, static strings, and functions. Keys have a maximum length of 512 characters, and keys longer than the maximum length will cause the action to fail.
|
||||
- `path`: **Required** The file path on the runner to cache or restore. The path can be an absolute path or relative to the working directory.
|
||||
- With `v2` of the `cache` action, you can specify a single path, or multiple paths as a list. Paths can be either directories or single files, and glob patterns are supported.
|
||||
- Paths can be either directories or single files, and glob patterns are supported.
|
||||
- With `v2` of the `cache` action, you can specify a single path, or you can add multiple paths on separate lines. For example:
|
||||
```
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
```
|
||||
- With `v1` of the `cache` action, only a single path is supported and it must be a directory. You cannot cache a single file.
|
||||
- `restore-keys`: **Optional** An ordered list of alternative keys to use for finding the cache if no cache hit occurred for `key`.
|
||||
|
||||
@@ -105,7 +114,6 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
```
|
||||
{% endraw %}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ topics:
|
||||
|
||||
### Introduction
|
||||
|
||||
This guide shows you workflow examples that configure a service container using the Docker Hub `postgres` image. The workflow runs a script to create a PostgreSQL client and populate the client with data. To test that the workflow creates and populates the PostgreSQL client, the script prints the client's data to the console.
|
||||
This guide shows you workflow examples that configure a service container using the Docker Hub `postgres` image. The workflow runs a script that connects to the PostgreSQL service, creates a table, and then populates it with data. To test that the workflow creates and populates the PostgreSQL table, the script prints the data from the table to the console.
|
||||
|
||||
{% data reusables.github-actions.docker-container-os-support %}
|
||||
|
||||
@@ -81,10 +81,10 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Connect to PostgreSQL
|
||||
# Runs a script that creates a PostgreSQL client, populates
|
||||
# the client with data, and retrieves data
|
||||
# Runs a script that creates a PostgreSQL table, populates
|
||||
# the table with data, and then retrieves the data.
|
||||
run: node client.js
|
||||
# Environment variable used by the `client.js` script to create a new PostgreSQL client.
|
||||
# Environment variables used by the `client.js` script to create a new PostgreSQL table.
|
||||
env:
|
||||
# The hostname used to communicate with the PostgreSQL service container
|
||||
POSTGRES_HOST: postgres
|
||||
@@ -141,8 +141,8 @@ steps:
|
||||
run: npm ci
|
||||
|
||||
- name: Connect to PostgreSQL
|
||||
# Runs a script that creates a PostgreSQL client, populates
|
||||
# the client with data, and retrieves data
|
||||
# Runs a script that creates a PostgreSQL table, populates
|
||||
# the table with data, and then retrieves the data.
|
||||
run: node client.js
|
||||
# Environment variable used by the `client.js` script to create
|
||||
# a new PostgreSQL client.
|
||||
@@ -204,11 +204,11 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Connect to PostgreSQL
|
||||
# Runs a script that creates a PostgreSQL client, populates
|
||||
# the client with data, and retrieves data
|
||||
# Runs a script that creates a PostgreSQL table, populates
|
||||
# the table with data, and then retrieves the data
|
||||
run: node client.js
|
||||
# Environment variable used by the `client.js` script to create
|
||||
# a new PostgreSQL client.
|
||||
# Environment variables used by the `client.js` script to create
|
||||
# a new PostgreSQL table.
|
||||
env:
|
||||
# The hostname used to communicate with the PostgreSQL service container
|
||||
POSTGRES_HOST: localhost
|
||||
@@ -268,11 +268,11 @@ steps:
|
||||
run: npm ci
|
||||
|
||||
- name: Connect to PostgreSQL
|
||||
# Runs a script that creates a PostgreSQL client, populates
|
||||
# the client with data, and retrieves data
|
||||
# Runs a script that creates a PostgreSQL table, populates
|
||||
# the table with data, and then retrieves the data
|
||||
run: node client.js
|
||||
# Environment variable used by the `client.js` script to create
|
||||
# a new PostgreSQL client.
|
||||
# Environment variables used by the `client.js` script to create
|
||||
# a new PostgreSQL table.
|
||||
env:
|
||||
# The hostname used to communicate with the PostgreSQL service container
|
||||
POSTGRES_HOST: localhost
|
||||
@@ -286,9 +286,9 @@ steps:
|
||||
|
||||
### Testing the PostgreSQL service container
|
||||
|
||||
You can test your workflow using the following script, which creates a PostgreSQL client and adds a new table with some placeholder data. The script then prints the values stored in the PostgreSQL client to the terminal. Your script can use any language you'd like, but this example uses Node.js and the `pg` npm module. For more information, see the [npm pg module](https://www.npmjs.com/package/pg).
|
||||
You can test your workflow using the following script, which connects to the PostgreSQL service and adds a new table with some placeholder data. The script then prints the values stored in the PostgreSQL table to the terminal. Your script can use any language you'd like, but this example uses Node.js and the `pg` npm module. For more information, see the [npm pg module](https://www.npmjs.com/package/pg).
|
||||
|
||||
You can modify *client.js* to include any PostgreSQL operations needed by your workflow. In this example, the script creates the PostgreSQL client instance, creates a table, adds placeholder data, then retrieves the data.
|
||||
You can modify *client.js* to include any PostgreSQL operations needed by your workflow. In this example, the script connects to the PostgreSQL service, adds a table to the `postgres` database, inserts some placeholder data, and then retrieves the data.
|
||||
|
||||
{% data reusables.github-actions.service-container-add-script %}
|
||||
|
||||
@@ -324,11 +324,11 @@ pgclient.query('SELECT * FROM student', (err, res) => {
|
||||
});
|
||||
```
|
||||
|
||||
The script creates a new PostgreSQL `Client`, which accepts a `host` and `port` parameter. The script uses the `POSTGRES_HOST` and `POSTGRES_PORT` environment variables to set the client's IP address and port. If `host` and `port` are not defined, the default host is `localhost` and the default port is 5432.
|
||||
The script creates a new connection to the PostgreSQL service, and uses the `POSTGRES_HOST` and `POSTGRES_PORT` environment variables to specify the PostgreSQL service IP address and port. If `host` and `port` are not defined, the default host is `localhost` and the default port is 5432.
|
||||
|
||||
The script creates a table and populates it with placeholder data. To test that the PostgreSQL database contains the data, the script prints the contents of the table to the console log.
|
||||
The script creates a table and populates it with placeholder data. To test that the `postgres` database contains the data, the script prints the contents of the table to the console log.
|
||||
|
||||
When you run this workflow, you should see the following output in the "Connect to PostgreSQL" step confirming you created the PostgreSQL client and added data:
|
||||
When you run this workflow, you should see the following output in the "Connect to PostgreSQL" step, which confirms that you successfully created the PostgreSQL table and added data:
|
||||
|
||||
```
|
||||
null [ { id: 1,
|
||||
|
||||
@@ -66,7 +66,7 @@ To remove a self-hosted runner from an organization, you must be an organization
|
||||
|
||||
{% if currentVersion == "free-pro-team@latest" %}
|
||||
To remove a self-hosted runner from an enterprise account, you must be an enterprise owner. We recommend that you also have access to the self-hosted runner machine.
|
||||
{% elsif enterpriseServerVersions contains currentVersion and currentVersion ver_gt "enterprise-server@2.21"% or currentVersion == "github-ae@latest" }
|
||||
{% elsif enterpriseServerVersions contains currentVersion and currentVersion ver_gt "enterprise-server@2.21" or currentVersion == "github-ae@latest" %}
|
||||
To remove a self-hosted runner at the enterprise level of {% data variables.product.product_location %}, you must be a site administrator. We recommend that you also have access to the self-hosted runner machine.
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ This list describes the recommended approaches for accessing repository data wit
|
||||
|
||||
**Self-hosted** runners on {% data variables.product.product_name %} do not have guarantees around running in ephemeral clean virtual machines, and can be persistently compromised by untrusted code in a workflow.
|
||||
|
||||
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 pull request (generally those with read-access to the repository) are able to compromise the self-hosted runner environment, including gaining access to secrets and the `GITHUB_TOKEN` which{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@3.1" or currentVersion == "github-ae@next" %}, depending on its settings, can grant {% else %} grants {% endif %}write-access permissions on the repository.
|
||||
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 pull request (generally those with read-access to the repository) are able to compromise the self-hosted runner environment, including gaining access to secrets and the `GITHUB_TOKEN` which{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@3.1" or currentVersion == "github-ae@next" %}, depending on its settings, can grant {% else %} grants {% endif %}write-access permissions on the repository. Although workflows can control access to environment secrets by using environments and required reviews, these workflows are not run in an isolated environment and are still susceptible to the same risks when run on a self-hosted runner.
|
||||
|
||||
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)."
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ You can use the `GITHUB_TOKEN` by using the standard syntax for referencing secr
|
||||
|
||||
This example workflow uses the [labeler action](https://github.com/actions/labeler), which requires the `GITHUB_TOKEN` as the value for the `repo-token` input parameter:
|
||||
|
||||
|
||||
```yaml
|
||||
name: Pull request labeler
|
||||
|
||||
@@ -64,7 +63,6 @@ jobs:
|
||||
repo-token: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %}
|
||||
```
|
||||
|
||||
|
||||
#### Example 2: calling the REST API
|
||||
|
||||
You can use the `GITHUB_TOKEN` to make authenticated API calls. This example workflow creates an issue using the {% data variables.product.prodname_dotcom %} REST API:
|
||||
|
||||
@@ -93,11 +93,11 @@ The `github` context contains information about the workflow run and the event t
|
||||
| `github.action` | `string` | The name of the action currently running. {% data variables.product.prodname_dotcom %} removes special characters or uses the name `run` when the current step runs a script. If you use the same action more than once in the same job, the name will include a suffix with the sequence number. For example, the first script you run will have the name `run1`, and the second script will be named `run2`. Similarly, the second invocation of `actions/checkout` will be `actionscheckout2`. |
|
||||
| `github.action_path` | `string` | The path where your action is located. You can use this path to easily access files located in the same repository as your action. This attribute is only supported in composite run steps actions. |
|
||||
| `github.actor` | `string` | The login of the user that initiated the workflow run. |
|
||||
| `github.base_ref` | `string` | The `base_ref` or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is a `pull_request`. |
|
||||
| `github.base_ref` | `string` | The `base_ref` or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either `pull_request` or `pull_request_target`. |
|
||||
| `github.event` | `object` | The full event webhook payload. For more information, see "[Events that trigger workflows](/articles/events-that-trigger-workflows/)." You can access individual properties of the event using this context. |
|
||||
| `github.event_name` | `string` | The name of the event that triggered the workflow run. |
|
||||
| `github.event_path` | `string` | The path to the full event webhook payload on the runner. |
|
||||
| `github.head_ref` | `string` | The `head_ref` or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is a `pull_request`. |
|
||||
| `github.head_ref` | `string` | The `head_ref` or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either `pull_request` or `pull_request_target`. |
|
||||
| `github.job` | `string` | The [`job_id`](/actions/reference/workflow-syntax-for-github-actions#jobsjob_id) of the current job. |
|
||||
| `github.ref` | `string` | The branch or tag ref that triggered the workflow run. For branches this in the format `refs/heads/<branch_name>`, and for tags it is `refs/tags/<tag_name>`. |
|
||||
| `github.repository` | `string` | The owner and repository name. For example, `Codertocat/Hello-World`. |
|
||||
|
||||
@@ -29,15 +29,9 @@ For secrets stored at the environment level, you can enable required reviewers t
|
||||
|
||||
#### Naming your secrets
|
||||
|
||||
The following rules apply to secret names:
|
||||
{% data reusables.codespaces.secrets-naming %}. For example, {% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@3.0" or currentVersion == "github-ae@latest" %}a secret created at the environment level must have a unique name in that environment, {% endif %}a secret created at the repository level must have a unique name in that repository, and a secret created at the organization level must have a unique name at that level.
|
||||
|
||||
* Secret names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed.
|
||||
* Secret names must not start with the `GITHUB_` prefix.
|
||||
* Secret names must not start with a number.
|
||||
* Secret names are not case-sensitive.
|
||||
* Secret names must be unique at the level they are created at. For example, {% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@3.0" or currentVersion == "github-ae@latest" %}a secret created at the environment level must have a unique name in that environment, {% endif %}a secret created at the repository level must have a unique name in that repository, and a secret created at the organization level must have a unique name at that level.
|
||||
|
||||
If a secret with the same name exists at multiple levels, the secret at the lower level takes precedence. For example, if an organization-level secret has the same name as a repository-level secret, then the repository-level secret takes precedence.{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@3.0" or currentVersion == "github-ae@latest" %} Similarly, if an organization, repository, and environment all have a secret with the same name, the environment-level secret takes precedence.{% endif %}
|
||||
{% data reusables.codespaces.secret-precedence %}{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@3.0" or currentVersion == "github-ae@latest" %} Similarly, if an organization, repository, and environment all have a secret with the same name, the environment-level secret takes precedence.{% endif %}
|
||||
|
||||
To help ensure that {% data variables.product.prodname_dotcom %} redacts your secret in logs, avoid using structured data as the values of secrets. For example, avoid creating secrets that contain JSON or encoded Git blobs.
|
||||
|
||||
@@ -83,7 +77,8 @@ If your repository {% if currentVersion == "free-pro-team@latest" or currentVers
|
||||
|
||||
{% endnote %}
|
||||
|
||||
{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@3.0" or currentVersion == "github-ae@latest" }
|
||||
{% if currentVersion == "free-pro-team@latest" or currentVersion ver_gt "enterprise-server@3.0" or currentVersion == "github-ae@latest" %}
|
||||
|
||||
### Creating encrypted secrets for an environment
|
||||
|
||||
{% data reusables.github-actions.permissions-statement-secrets-environment %}
|
||||
|
||||
@@ -58,6 +58,7 @@ We strongly recommend that actions use environment variables to access the files
|
||||
| `GITHUB_RUN_NUMBER` | {% data reusables.github-actions.run_number_description %} |
|
||||
| `GITHUB_JOB` | The [job_id](/actions/reference/workflow-syntax-for-github-actions#jobsjob_id) of the current job. |
|
||||
| `GITHUB_ACTION` | The unique identifier (`id`) of the action. |
|
||||
| `GITHUB_ACTION_PATH` | The path where your action is located. You can use this path to access files located in the same repository as your action. This variable is only supported in composite run steps actions. |
|
||||
| `GITHUB_ACTIONS` | Always set to `true` when {% data variables.product.prodname_actions %} is running the workflow. You can use this variable to differentiate when tests are being run locally or by {% data variables.product.prodname_actions %}.
|
||||
| `GITHUB_ACTOR` | The name of the person or app that initiated the workflow. For example, `octocat`. |
|
||||
| `GITHUB_REPOSITORY` | The owner and repository name. For example, `octocat/Hello-World`. |
|
||||
|
||||
@@ -48,6 +48,12 @@ Use deployment branches to restrict which branches can deploy to the environment
|
||||
|
||||
Secrets stored in an environment are only available to workflow jobs that reference the environment. If the environment requires approval, a job cannot access environment secrets until one of the required reviewers approves it. For more information about secrets, see "[Encrypted secrets](/actions/reference/encrypted-secrets)."
|
||||
|
||||
{% note %}
|
||||
|
||||
**Note:** Workflows that run on self-hosted runners are not run in an isolated container, even if they use environments. Environment secrets should be treated with the same level as security as repository and organization secrets. For more information, see "[Security hardening for GitHub Actions](/actions/learn-github-actions/security-hardening-for-github-actions#hardening-for-self-hosted-runners)."
|
||||
|
||||
{% endnote %}
|
||||
|
||||
### Creating an environment
|
||||
|
||||
{% data reusables.github-actions.permissions-statement-environment %}
|
||||
@@ -83,7 +89,7 @@ Deleting an environment will delete all secrets and protection rules associated
|
||||
{% data reusables.repositories.navigate-to-repo %}
|
||||
{% data reusables.repositories.sidebar-settings %}
|
||||
{% data reusables.github-actions.sidebar-environment %}
|
||||
1. Next the the environment that you want to delete, click {% octicon "trash" aria-label="The trash icon" %}.
|
||||
1. Next to the environment that you want to delete, click {% octicon "trash" aria-label="The trash icon" %}.
|
||||
2. Click **I understand, delete this environment**.
|
||||
|
||||
{% if currentVersion == "free-pro-team@latest" or currentVersion == "github-ae@next" or currentVersion ver_gt "enterprise-server@3.1" %}You can also delete environments through the REST API. For more information, see "[Environments](/rest/reference/repos#environments)."{% endif %}
|
||||
|
||||
@@ -177,16 +177,16 @@ Runs your workflow anytime the `check_run` event occurs. {% data reusables.devel
|
||||
|
||||
| Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF` |
|
||||
| --------------------- | -------------- | ------------ | -------------|
|
||||
| [`check_run`](/webhooks/event-payloads/#check_run) | - `created`<br/>- `rerequested`<br/>- `completed`<br/>- `requested_action` | Last commit on default branch | Default branch |
|
||||
| [`check_run`](/webhooks/event-payloads/#check_run) | - `created`<br/>- `rerequested`<br/>- `completed` | Last commit on default branch | Default branch |
|
||||
|
||||
{% data reusables.developer-site.limit_workflow_to_activity_types %}
|
||||
|
||||
For example, you can run a workflow when a check run has been `rerequested` or `requested_action`.
|
||||
For example, you can run a workflow when a check run has been `rerequested` or `completed`.
|
||||
|
||||
```yaml
|
||||
on:
|
||||
check_run:
|
||||
types: [rerequested, requested_action]
|
||||
types: [rerequested, completed]
|
||||
```
|
||||
|
||||
#### `check_suite`
|
||||
|
||||
@@ -725,7 +725,9 @@ jobs:
|
||||
|
||||
#### Example using action inside a different private repository than the workflow
|
||||
|
||||
Your workflow must checkout the private repository and reference the action locally.
|
||||
Your workflow must checkout the private repository and reference the action locally. Generate a personal access token and add the token as an encrypted secret. For more information, see "[Creating a personal access token](/github/authenticating-to-github/creating-a-personal-access-token)" and "[Encrypted secrets](/actions/reference/encrypted-secrets)."
|
||||
|
||||
Replace `PERSONAL_ACCESS_TOKEN` in the example with the name of your secret.
|
||||
|
||||
{% raw %}
|
||||
```yaml
|
||||
@@ -737,7 +739,7 @@ jobs:
|
||||
with:
|
||||
repository: octocat/my-private-repo
|
||||
ref: v1.0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
path: ./.github/actions/my-private-repo
|
||||
- name: Run my action
|
||||
uses: ./.github/actions/my-private-repo/my-action
|
||||
|
||||
@@ -4,8 +4,12 @@ intro: 'You need a license to use {% data variables.product.prodname_GH_advanced
|
||||
product: '{% data reusables.gated-features.ghas %}'
|
||||
versions:
|
||||
enterprise-server: '>=3.1'
|
||||
type: overview
|
||||
topics:
|
||||
- Advanced Security
|
||||
- Enterprise
|
||||
- Licensing
|
||||
- Security
|
||||
---
|
||||
|
||||
### About licensing for {% data variables.product.prodname_GH_advanced_security %}
|
||||
|
||||
@@ -9,8 +9,12 @@ redirect_from:
|
||||
- /admin/configuration/configuring-code-scanning-for-your-appliance
|
||||
versions:
|
||||
enterprise-server: '>=2.22'
|
||||
type: how_to
|
||||
topics:
|
||||
- Advanced Security
|
||||
- Code scanning
|
||||
- Enterprise
|
||||
- Security
|
||||
---
|
||||
|
||||
{% data reusables.code-scanning.beta %}
|
||||
|
||||