From 1f7fff816f82cf50e3b2aceddab8b8fd22845f78 Mon Sep 17 00:00:00 2001 From: Jenni Christensen <97056108+dihydroJenoxide@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:41:50 -0700 Subject: [PATCH 1/4] Clean up and combine notes in actions events article (#57215) Co-authored-by: hubwriter --- .../events-that-trigger-workflows.md | 176 ++++++------------ data/reusables/actions/branch-requirement.md | 3 +- .../webhooks/discussions-webhooks-beta.md | 3 +- 3 files changed, 64 insertions(+), 118 deletions(-) diff --git a/content/actions/reference/workflows-and-actions/events-that-trigger-workflows.md b/content/actions/reference/workflows-and-actions/events-that-trigger-workflows.md index 6f5d0f024b7c..a9409f5d839d 100644 --- a/content/actions/reference/workflows-and-actions/events-that-trigger-workflows.md +++ b/content/actions/reference/workflows-and-actions/events-that-trigger-workflows.md @@ -31,9 +31,8 @@ Some events have multiple activity types. For these events, you can specify whic | [`branch_protection_rule`](/webhooks-and-events/webhooks/webhook-events-and-payloads#branch_protection_rule) | - `created`
- `edited`
- `deleted` | Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#branch_protection_rule). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#branch_protection_rule). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} Runs your workflow when branch protection rules in the workflow repository are changed. For more information about branch protection rules, see [AUTOTITLE](/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches). For information about the branch protection rule APIs, see [AUTOTITLE](/graphql/reference/objects#branchprotectionrule) in the GraphQL API documentation or [AUTOTITLE](/rest/branches). @@ -52,12 +51,9 @@ on: | [`check_run`](/webhooks-and-events/webhooks/webhook-events-and-payloads#check_run) | - `created`
- `rerequested`
- `completed`
- `requested_action` | Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#check_run). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} - -> [!NOTE] -> To prevent recursive workflows, this event does not trigger workflows if the check run's check suite was created by {% data variables.product.prodname_actions %} or if the check suite's head SHA is associated with {% data variables.product.prodname_actions %}. +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#check_run). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} +> * To prevent recursive workflows, this event does not trigger workflows if the check run's check suite was created by {% data variables.product.prodname_actions %} or if the check suite's head SHA is associated with {% data variables.product.prodname_actions %}. Runs your workflow when activity related to a check run occurs. A check run is an individual test that is part of a check suite. For information, see [AUTOTITLE](/rest/guides/using-the-rest-api-to-interact-with-checks). For information about the check run APIs, see [AUTOTITLE](/graphql/reference/objects#checkrun) in the GraphQL API documentation or [AUTOTITLE](/rest/checks/runs). @@ -76,12 +72,9 @@ on: | [`check_suite`](/webhooks-and-events/webhooks/webhook-events-and-payloads#check_suite) | - `completed` | Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#check_suite). Although only the `completed` activity type is supported, specifying the activity type will keep your workflow specific if more activity types are added in the future. {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} - -> [!NOTE] -> To prevent recursive workflows, this event does not trigger workflows if the check suite was created by {% data variables.product.prodname_actions %} or if the check suite's head SHA is associated with {% data variables.product.prodname_actions %}. +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#check_suite). Although only the `completed` activity type is supported, specifying the activity type will keep your workflow specific if more activity types are added in the future. {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} +> * To prevent recursive workflows, this event does not trigger workflows if the check suite was created by {% data variables.product.prodname_actions %} or if the check suite's head SHA is associated with {% data variables.product.prodname_actions %}. Runs your workflow when check suite activity occurs. A check suite is a collection of the check runs created for a specific commit. Check suites summarize the status and conclusion of the check runs that are in the suite. For information, see [AUTOTITLE](/rest/guides/using-the-rest-api-to-interact-with-checks). For information about the check suite APIs, see [AUTOTITLE](/graphql/reference/objects#checksuite) in the GraphQL API documentation or [AUTOTITLE](/rest/checks/suites). @@ -117,10 +110,9 @@ on: | --------------------- | -------------- | ------------ | -------------| | [`delete`](/webhooks-and-events/webhooks/webhook-events-and-payloads#delete) | Not applicable | Last commit on default branch | Default branch | -{% data reusables.actions.branch-requirement %} - > [!NOTE] -> An event will not be created when you delete more than three tags at once. +> * {% data reusables.actions.branch-requirement %} +> * An event will not be created when you delete more than three tags at once. Runs your workflow when someone deletes a Git reference (Git branch or tag) in the workflow's repository. For information about the APIs to delete a Git reference, see [AUTOTITLE](/graphql/reference/mutations#deleteref) in the GraphQL API documentation or [AUTOTITLE](/rest/git/refs#delete-a-reference). @@ -171,11 +163,9 @@ on: | [`discussion`](/webhooks-and-events/webhooks/webhook-events-and-payloads#discussion) | - `created`
- `edited`
- `deleted`
- `transferred`
- `pinned`
- `unpinned`
- `labeled`
- `unlabeled`
- `locked`
- `unlocked`
- `category_changed`
- `answered`
- `unanswered` | Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#discussion). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} - -{% data reusables.webhooks.discussions-webhooks-beta %} +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#discussion). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} +> * {% data reusables.webhooks.discussions-webhooks-beta %} Runs your workflow when a discussion in the workflow's repository is created or modified. For activity related to comments on a discussion, use the [`discussion_comment`](#discussion_comment) event. For more information about discussions, see [AUTOTITLE](/discussions/collaborating-with-your-community-using-discussions/about-discussions). For information about the GraphQL API, see [AUTOTITLE](/graphql/reference/objects#discussion). @@ -194,11 +184,9 @@ on: | [`discussion_comment`](/webhooks-and-events/webhooks/webhook-events-and-payloads#discussion_comment) | - `created`
- `edited`
- `deleted`
| Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#discussion_comment). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} - -{% data reusables.webhooks.discussions-webhooks-beta %} +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#discussion_comment). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} +> * {% data reusables.webhooks.discussions-webhooks-beta %} Runs your workflow when a comment on a discussion in the workflow's repository is created or modified. For activity related to a discussion as opposed to comments on the discussion, use the [`discussion`](#discussion) event. For more information about discussions, see [AUTOTITLE](/discussions/collaborating-with-your-community-using-discussions/about-discussions). For information about the GraphQL API, see [AUTOTITLE](/graphql/reference/objects#discussion). @@ -216,7 +204,8 @@ on: | --------------------- | -------------- | ------------ | -------------| | [`fork`](/webhooks-and-events/webhooks/webhook-events-and-payloads#fork) | Not applicable | Last commit on default branch | Default branch | -{% data reusables.actions.branch-requirement %} +> [!NOTE] +> {% data reusables.actions.branch-requirement %} Runs your workflow when someone forks a repository. For information about the REST API, see [AUTOTITLE](/rest/repos/forks#create-a-fork). @@ -233,7 +222,8 @@ on: | --------------------- | -------------- | ------------ | -------------| | [`gollum`](/webhooks-and-events/webhooks/webhook-events-and-payloads#gollum) | Not applicable | Last commit on default branch | Default branch | -{% data reusables.actions.branch-requirement %} +> [!NOTE] +> {% data reusables.actions.branch-requirement %} Runs your workflow when someone creates or updates a Wiki page. For more information, see [AUTOTITLE](/communities/documenting-your-project-with-wikis/about-wikis). @@ -251,9 +241,8 @@ on: | [`issue_comment`](/webhooks-and-events/webhooks/webhook-events-and-payloads#issue_comment) | - `created`
- `edited`
- `deleted`
| Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#issue_comment). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#issue_comment). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} Runs your workflow when an issue or pull request comment is created, edited, or deleted. For information about the issue comment APIs, see [AUTOTITLE](/graphql/reference/objects#issuecomment) in the GraphQL API documentation or [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#issue_comment) in the REST API documentation. @@ -305,9 +294,8 @@ jobs: | [`issues`](/webhooks-and-events/webhooks/webhook-events-and-payloads#issues) | - `opened`
- `edited`
- `deleted`
- `transferred`
- `pinned`
- `unpinned`
- `closed`
- `reopened`
- `assigned`
- `unassigned`
- `labeled`
- `unlabeled`
- `locked`
- `unlocked`
- `milestoned`
- `demilestoned`
- `typed`
- `untyped` | Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#issues). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#issues). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} Runs your workflow when an issue in the workflow's repository is created or modified. For activity related to comments in an issue, use the [`issue_comment`](#issue_comment) event. For more information about issues, see [AUTOTITLE](/issues/tracking-your-work-with-issues/about-issues). For information about the issue APIs, see [AUTOTITLE](/graphql/reference/objects#issue) in the GraphQL API documentation or [AUTOTITLE](/rest/issues). @@ -326,9 +314,8 @@ on: | [`label`](/webhooks-and-events/webhooks/webhook-events-and-payloads#label) | - `created`
- `edited`
- `deleted`
| Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#label). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#label). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} Runs your workflow when a label in your workflow's repository is created or modified. For more information about labels, see [AUTOTITLE](/issues/using-labels-and-milestones-to-track-work/managing-labels). For information about the label APIs, see [AUTOTITLE](/graphql/reference/objects#label) in the GraphQL API documentation or [AUTOTITLE](/rest/issues/labels). @@ -349,6 +336,7 @@ on: | [`merge_group`](/webhooks-and-events/webhooks/webhook-events-and-payloads#merge_group) | `checks_requested` | SHA of the merge group | Ref of the merge group | > [!NOTE] +> > * {% data reusables.developer-site.multiple_activity_types %} Although only the `checks_requested` activity type is supported, specifying the activity type will keep your workflow specific if more activity types are added in the future. For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#merge_group). {% data reusables.developer-site.limit_workflow_to_activity_types %} > * {% data reusables.actions.merge-group-event-with-required-checks %} @@ -371,9 +359,8 @@ on: | [`milestone`](/webhooks-and-events/webhooks/webhook-events-and-payloads#milestone) | - `created`
- `closed`
- `opened`
- `edited`
- `deleted`
| Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#milestone). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#milestone). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} Runs your workflow when a milestone in the workflow's repository is created or modified. For more information about milestones, see [AUTOTITLE](/issues/using-labels-and-milestones-to-track-work/about-milestones). For information about the milestone APIs, see [AUTOTITLE](/graphql/reference/objects#milestone) in the GraphQL API documentation or [AUTOTITLE](/rest/issues/milestones). @@ -393,7 +380,8 @@ on: | --------------------- | -------------- | ------------ | -------------| | [`page_build`](/webhooks-and-events/webhooks/webhook-events-and-payloads#page_build) | Not applicable | Last commit on default branch | Default branch | -{% data reusables.actions.branch-requirement %} +> [!NOTE] +> {% data reusables.actions.branch-requirement %} Runs your workflow when someone pushes to a branch that is the publishing source for {% data variables.product.prodname_pages %}, if {% data variables.product.prodname_pages %} is enabled for the repository. For more information about {% data variables.product.prodname_pages %} publishing sources, see [AUTOTITLE](/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site). For information about the REST API, see [AUTOTITLE](/rest/repos#pages). @@ -413,19 +401,9 @@ on: | [`project`](/webhooks-and-events/webhooks/webhook-events-and-payloads#project) | - `created`
- `closed`
- `reopened`
- `edited`
- `deleted`
| Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} The `edited` activity type refers to when a {% data variables.projects.projects_v1_board %}, not a column or card on the {% data variables.projects.projects_v1_board %}, is edited. For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#project). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} - -> [!NOTE] -> This event only occurs for projects owned by the workflow's repository, not for organization-owned or user-owned projects or for projects owned by another repository. - -{% ifversion fpt or ghec %} - -> [!NOTE] -> This event only occurs for {% data variables.product.prodname_projects_v1 %}. - -{% endif %} +> * {% data reusables.developer-site.multiple_activity_types %} The `edited` activity type refers to when a {% data variables.projects.projects_v1_board %}, not a column or card on the {% data variables.projects.projects_v1_board %}, is edited. For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#project). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} +> * This event only occurs for projects owned by the workflow's repository, not for organization-owned or user-owned projects or for projects owned by another repository. Runs your workflow when a {% data variables.projects.projects_v1_board %} is created or modified. For activity related to cards or columns in a {% data variables.projects.projects_v1_board %}, use the [`project_card`](#project_card) or [`project_column`](#project_column) events instead. For more information about {% data variables.projects.projects_v1_boards %}, see [AUTOTITLE](/issues/organizing-your-work-with-project-boards/managing-project-boards/about-project-boards). For information about the {% data variables.projects.projects_v1_board %} APIs, see [AUTOTITLE](/graphql/reference/objects#project) in the GraphQL API documentation or [AUTOTITLE](/rest/projects). @@ -444,19 +422,9 @@ on: | [`project_card`](/webhooks-and-events/webhooks/webhook-events-and-payloads#project_card) | - `created`
- `moved`
- `converted` to an issue
- `edited`
- `deleted` | Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#project_card). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} - -> [!NOTE] -> This event only occurs for projects owned by the workflow's repository, not for organization-owned or user-owned projects or for projects owned by another repository. - -{% ifversion fpt or ghec %} - -> [!NOTE] -> This event only occurs for {% data variables.product.prodname_projects_v1 %}. - -{% endif %} +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#project_card). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} +> * This event only occurs for projects owned by the workflow's repository, not for organization-owned or user-owned projects or for projects owned by another repository. Runs your workflow when a card on a {% data variables.projects.projects_v1_board %} is created or modified. For activity related to {% data variables.projects.projects_v1_boards %} or columns in a {% data variables.projects.projects_v1_board %}, use the [`project`](#project) or [`project_column`](#project_column) event instead. For more information about {% data variables.projects.projects_v1_boards %}, see [AUTOTITLE](/issues/organizing-your-work-with-project-boards/managing-project-boards/about-project-boards). For information about the project card APIs, see [AUTOTITLE](/graphql/reference/objects#projectcard) in the GraphQL API documentation or [AUTOTITLE](/rest/projects/cards). @@ -475,19 +443,9 @@ on: | [`project_column`](/webhooks-and-events/webhooks/webhook-events-and-payloads#project_column) | - `created`
- `updated`
- `moved`
- `deleted` | Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#project_column). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} - -> [!NOTE] -> This event only occurs for projects owned by the workflow's repository, not for organization-owned or user-owned projects or for projects owned by another repository. - -{% ifversion fpt or ghec %} - -> [!NOTE] -> This event only occurs for {% data variables.product.prodname_projects_v1 %}. - -{% endif %} +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#project_column). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} +> * This event only occurs for projects owned by the workflow's repository, not for organization-owned or user-owned projects or for projects owned by another repository. Runs your workflow when a column on a {% data variables.projects.projects_v1_board %} is created or modified. For activity related to {% data variables.projects.projects_v1_boards %} or cards in a {% data variables.projects.projects_v1_board %}, use the [`project`](#project) or [`project_card`](#project_card) event instead. For more information about {% data variables.projects.projects_v1_boards %}, see [AUTOTITLE](/issues/organizing-your-work-with-project-boards/managing-project-boards/about-project-boards). For information about the project column APIs, see [AUTOTITLE](/graphql/reference/objects#projectcolumn) in the GraphQL API documentation or [AUTOTITLE](/rest/projects#columns). @@ -507,7 +465,8 @@ on: | --------------------- | -------------- | ------------ | -------------| | [`public`](/webhooks-and-events/webhooks/webhook-events-and-payloads#public) | Not applicable | Last commit on default branch | Default branch | -{% data reusables.actions.branch-requirement %} +> [!NOTE] +> {% data reusables.actions.branch-requirement %} Runs your workflow when your workflow's repository changes from private to public. For information about the REST API, see [AUTOTITLE](/rest/repos#edit). @@ -526,8 +485,7 @@ on: > [!NOTE] > * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request). By default, a workflow only runs when a `pull_request` event's activity type is `opened`, `synchronize`, or `reopened`. To trigger workflows by different activity types, use the `types` keyword. For more information, see [AUTOTITLE](/actions/using-workflows/workflow-syntax-for-github-actions#onevent_nametypes). -> * Workflows will not run on `pull_request` activity if the pull request has a merge conflict. The merge conflict must be resolved first. -> Conversely, workflows with the `pull_request_target` event will run even if the pull request has a merge conflict. Before using the `pull_request_target` trigger, you should be aware of the security risks. For more information, see [`pull_request_target`](#pull_request_target). +> * Workflows will not run on `pull_request` activity if the pull request has a merge conflict. The merge conflict must be resolved first. Conversely, workflows with the `pull_request_target` event will run even if the pull request has a merge conflict. Before using the `pull_request_target` trigger, you should be aware of the security risks. For more information, see [`pull_request_target`](#pull_request_target). > * The `pull_request` webhook event payload is empty for merged pull requests and pull requests that come from forked repositories. > * The value of `GITHUB_REF` varies for a closed pull request depending on whether the pull request has been merged or not. If a pull request was closed but not merged, it will be `refs/pull/PULL_REQUEST_NUMBER/merge`. If a pull request was closed as a result of being merged, it will be the fully qualified `ref` of the branch it was merged into, for example `/refs/heads/main`. @@ -834,10 +792,8 @@ jobs: | [`push`](/webhooks-and-events/webhooks/webhook-events-and-payloads#push) | Not applicable | Tip commit pushed to the ref. When you delete a branch, the SHA in the workflow run (and its associated refs) reverts to the default branch of the repository. | Updated ref | > [!NOTE] -> The webhook payload available to GitHub Actions does not include the `added`, `removed`, and `modified` attributes in the `commit` object. You can retrieve the full commit object using the API. For information, see [AUTOTITLE](/graphql/reference/objects#commit) in the GraphQL API documentation or [AUTOTITLE](/rest/commits#get-a-commit). - -> [!NOTE] -> {% ifversion fpt or ghec or ghes > 3.14 %}Events will not be created if more than 5,000 branches are pushed at once. {% endif %}Events will not be created for tags when more than three tags are pushed at once. +> * The webhook payload available to GitHub Actions does not include the `added`, `removed`, and `modified` attributes in the `commit` object. You can retrieve the full commit object using the API. For information, see [AUTOTITLE](/graphql/reference/objects#commit) in the GraphQL API documentation or [AUTOTITLE](/rest/commits#get-a-commit). +> * {% ifversion fpt or ghec or ghes > 3.14 %}Events will not be created if more than 5,000 branches are pushed at once. {% endif %}Events will not be created for tags when more than three tags are pushed at once. Runs your workflow when you push a commit or tag, or when you create a repository from a template. @@ -910,12 +866,9 @@ on: | [`registry_package`](/webhooks-and-events/webhooks/webhook-events-and-payloads#package) | - `published`
- `updated` | Commit of the published package | Branch or tag of the published package | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#registry_package). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} - -> [!NOTE] -> When pushing multi-architecture container images, this event occurs once per manifest, so you might observe your workflow triggering multiple times. To mitigate this, and only run your workflow job for the event that contains the actual image tag information, use a conditional: +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#registry_package). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} +> * When pushing multi-architecture container images, this event occurs once per manifest, so you might observe your workflow triggering multiple times. To mitigate this, and only run your workflow job for the event that contains the actual image tag information, use a conditional: > > ```yaml > jobs: @@ -940,13 +893,9 @@ on: | [`release`](/webhooks-and-events/webhooks/webhook-events-and-payloads#release) | - `published`
- `unpublished`
- `created`
- `edited`
- `deleted`
- `prereleased`
- `released` | Last commit in the tagged release | Tag ref of release `refs/tags/` | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#release). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -> [!NOTE] -> Workflows are not triggered for the `created`, `edited`, or `deleted` activity types for draft releases. When you create your release through the {% data variables.product.github %} UI, your release may automatically be saved as a draft. - -> [!NOTE] -> The `prereleased` type will not trigger for pre-releases published from draft releases, but the `published` type will trigger. If you want a workflow to run when stable _and_ pre-releases publish, subscribe to `published` instead of `released` and `prereleased`. +> * {% data reusables.developer-site.multiple_activity_types %} For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#release). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * Workflows are not triggered for the `created`, `edited`, or `deleted` activity types for draft releases. When you create your release through the {% data variables.product.github %} UI, your release may automatically be saved as a draft. +> * The `prereleased` type will not trigger for pre-releases published from draft releases, but the `published` type will trigger. If you want a workflow to run when stable _and_ pre-releases publish, subscribe to `published` instead of `released` and `prereleased`. Runs your workflow when release activity in your repository occurs. For information about the release APIs, see [AUTOTITLE](/graphql/reference/objects#release) in the GraphQL API documentation or [AUTOTITLE](/rest/releases) in the REST API documentation. @@ -964,7 +913,8 @@ on: | ------------------ | ------------ | ------------ | ------------------| | [repository_dispatch](/webhooks-and-events/webhooks/webhook-events-and-payloads#repository_dispatch) | Custom | Last commit on default branch | Default branch | -{% data reusables.actions.branch-requirement %} +> [!NOTE] +> {% data reusables.actions.branch-requirement %} You can use the {% data variables.product.github %} API to trigger a webhook event called [`repository_dispatch`](/webhooks-and-events/webhooks/webhook-events-and-payloads#repository_dispatch) when you want to trigger a workflow for activity that happens outside of {% data variables.product.github %}. For more information, see [AUTOTITLE](/rest/repos/repos#create-a-repository-dispatch-event). @@ -1075,7 +1025,8 @@ Notifications for scheduled workflows are sent to the user who last modified the | --------------------- | -------------- | ------------ | -------------| | [`status`](/webhooks-and-events/webhooks/webhook-events-and-payloads#status) | Not applicable | Last commit on default branch | Default branch | -{% data reusables.actions.branch-requirement %} +> [!NOTE] +> {% data reusables.actions.branch-requirement %} Runs your workflow when the status of a Git commit changes. For example, commits can be marked as `error`, `failure`, `pending`, or `success`. If you want to provide more details about the status change, you may want to use the [`check_run`](#check_run) event. For information about the commit status APIs, see [AUTOTITLE](/graphql/reference/objects#status) in the GraphQL API documentation or [AUTOTITLE](/rest/commits#commit-statuses). @@ -1111,9 +1062,8 @@ jobs: | [`watch`](/webhooks-and-events/webhooks/webhook-events-and-payloads#watch) | - `started` | Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} Although only the `started` activity type is supported, specifying the activity type will keep your workflow specific if more activity types are added in the future. For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#watch). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} +> * {% data reusables.developer-site.multiple_activity_types %} Although only the `started` activity type is supported, specifying the activity type will keep your workflow specific if more activity types are added in the future. For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#watch). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} Runs your workflow when the workflow's repository is starred. For information about the pull request APIs, see [AUTOTITLE](/graphql/reference/mutations#addstar) in the GraphQL API documentation or [AUTOTITLE](/rest/activity/starring). @@ -1145,7 +1095,8 @@ on: workflow_call | ------------------ | ------------ | ------------ | ------------------| | [workflow_dispatch](/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_dispatch) | Not applicable | Last commit on the `GITHUB_REF` branch or tag | Branch or tag that received dispatch | -{% data reusables.actions.branch-requirement %} +> [!NOTE] +> {% data reusables.actions.branch-requirement %} To enable a workflow to be triggered manually, you need to configure the `workflow_dispatch` event. You can manually trigger a workflow run using the {% data variables.product.github %} API, {% data variables.product.prodname_cli %}, or the {% data variables.product.github %} UI. For more information, see [AUTOTITLE](/actions/managing-workflow-runs/manually-running-a-workflow). @@ -1216,12 +1167,9 @@ For more information, see the {% data variables.product.prodname_cli %} informat | [`workflow_run`](/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_run) | - `completed`
- `requested`
- `in_progress` | Last commit on default branch | Default branch | > [!NOTE] -> {% data reusables.developer-site.multiple_activity_types %} The `requested` activity type does not occur when a workflow is re-run. For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_run). {% data reusables.developer-site.limit_workflow_to_activity_types %} - -{% data reusables.actions.branch-requirement %} - -> [!NOTE] -> You can't use `workflow_run` to chain together more than three levels of workflows. For example, if you attempt to trigger five workflows (named `B` to `F`) to run sequentially after an initial workflow `A` has run (that is: `A` → `B` → `C` → `D` → `E` → `F`), workflows `E` and `F` will not be run. +> * {% data reusables.developer-site.multiple_activity_types %} The `requested` activity type does not occur when a workflow is re-run. For information about each activity type, see [AUTOTITLE](/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_run). {% data reusables.developer-site.limit_workflow_to_activity_types %} +> * {% data reusables.actions.branch-requirement %} +> * You can't use `workflow_run` to chain together more than three levels of workflows. For example, if you attempt to trigger five workflows (named `B` to `F`) to run sequentially after an initial workflow `A` has run (that is: `A` → `B` → `C` → `D` → `E` → `F`), workflows `E` and `F` will not be run. This event occurs when a workflow run is requested or completed. It allows you to execute a workflow based on execution or completion of another workflow. The workflow started by the `workflow_run` event is able to access secrets and write tokens, even if the previous workflow was not. This is useful in cases where the previous workflow is intentionally not privileged, but you need to take a privileged action in a later workflow. diff --git a/data/reusables/actions/branch-requirement.md b/data/reusables/actions/branch-requirement.md index 4bc2eb84d36d..19cf2c8c6c9b 100644 --- a/data/reusables/actions/branch-requirement.md +++ b/data/reusables/actions/branch-requirement.md @@ -1,2 +1 @@ -> [!NOTE] -> This event will only trigger a workflow run if the workflow file exists on the default branch. +This event will only trigger a workflow run if the workflow file exists on the default branch. diff --git a/data/reusables/webhooks/discussions-webhooks-beta.md b/data/reusables/webhooks/discussions-webhooks-beta.md index 5405a5afd497..2eed6520949c 100644 --- a/data/reusables/webhooks/discussions-webhooks-beta.md +++ b/data/reusables/webhooks/discussions-webhooks-beta.md @@ -1,2 +1 @@ -> [!NOTE] -> Webhook events for {% data variables.product.prodname_discussions %} are currently in {% data variables.release-phases.public_preview %} and subject to change. +Webhook events for {% data variables.product.prodname_discussions %} are currently in {% data variables.release-phases.public_preview %} and subject to change. From 7d8b9cf28e63d9d4c1532dbeef7a5ec3dfbd2f57 Mon Sep 17 00:00:00 2001 From: Mardav Wala Date: Thu, 21 Aug 2025 13:06:03 -0400 Subject: [PATCH 2/4] feat: Implement App Router integration and 404 handling (#56915) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/404/page.tsx | 105 +++++++++++ src/app/_not-found/page.tsx | 7 + src/app/client-layout.tsx | 126 +++++++++++++ src/app/components/AppRouterMainContext.tsx | 52 ++++++ src/app/components/MainContextProvider.tsx | 17 ++ src/app/components/NotFoundContent.tsx | 119 ++++++++++++ src/app/layout.tsx | 54 ++++++ src/app/lib/app-router-context.ts | 87 +++++++++ src/app/lib/locale-context.tsx | 121 ++++++++++++ src/app/lib/main-context-adapter.ts | 108 +++++++++++ src/app/lib/routing-patterns.ts | 118 ++++++++++++ src/app/lib/use-detect-locale.tsx | 61 ++++++ src/app/not-found.tsx | 27 +++ src/app/types.ts | 83 +++++++++ src/app/validators.ts | 16 ++ src/frame/lib/header-utils.ts | 84 +++++++++ src/frame/middleware/app-router-gateway.ts | 114 ++++++++++++ src/frame/middleware/helmet.ts | 31 ++- src/frame/middleware/index.ts | 4 + src/frame/middleware/render-page.ts | 68 +++---- src/languages/components/useTranslation.ts | 81 +------- src/languages/lib/client-languages.ts | 67 +++++++ .../lib/correct-translation-content.ts | 26 +++ src/languages/lib/translation-utils.ts | 176 ++++++++++++++++++ src/pages/404.tsx | 36 ---- src/pages/_notfound.tsx | 5 - tsconfig.json | 24 ++- 27 files changed, 1653 insertions(+), 164 deletions(-) create mode 100644 src/app/404/page.tsx create mode 100644 src/app/_not-found/page.tsx create mode 100644 src/app/client-layout.tsx create mode 100644 src/app/components/AppRouterMainContext.tsx create mode 100644 src/app/components/MainContextProvider.tsx create mode 100644 src/app/components/NotFoundContent.tsx create mode 100644 src/app/layout.tsx create mode 100644 src/app/lib/app-router-context.ts create mode 100644 src/app/lib/locale-context.tsx create mode 100644 src/app/lib/main-context-adapter.ts create mode 100644 src/app/lib/routing-patterns.ts create mode 100644 src/app/lib/use-detect-locale.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/app/types.ts create mode 100644 src/app/validators.ts create mode 100644 src/frame/lib/header-utils.ts create mode 100644 src/frame/middleware/app-router-gateway.ts create mode 100644 src/languages/lib/client-languages.ts create mode 100644 src/languages/lib/translation-utils.ts delete mode 100644 src/pages/404.tsx delete mode 100644 src/pages/_notfound.tsx diff --git a/src/app/404/page.tsx b/src/app/404/page.tsx new file mode 100644 index 000000000000..20eed3fcc857 --- /dev/null +++ b/src/app/404/page.tsx @@ -0,0 +1,105 @@ +import { getAppRouterContext } from '@/app/lib/app-router-context' +import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext' +import { translate } from '@/languages/lib/translation-utils' +import { CommentDiscussionIcon, MarkGithubIcon } from '@primer/octicons-react' +import type { Metadata } from 'next' + +export const dynamic = 'force-dynamic' + +export const metadata: Metadata = { + title: '404 - Page not found', + other: { status: '404' }, +} + +export default async function Page404() { + // Get context with UI data + const appContext = await getAppRouterContext() + + const siteTitle = translate(appContext.site.data.ui, 'header.github_docs', 'GitHub Docs') + const oopsTitle = translate(appContext.site.data.ui, 'meta.oops', 'Ooops!') + + return ( + +
+ {/* Simple Header */} + + + {/* Main Content */} +
+
+

{oopsTitle}

+
+ It looks like this page doesn't exist. +
+

+ We track these errors automatically, but if the problem persists please feel free to + contact us. +

+ + + Contact support + +
+
+ + +
+
+ ) +} diff --git a/src/app/_not-found/page.tsx b/src/app/_not-found/page.tsx new file mode 100644 index 000000000000..562bf64ed89e --- /dev/null +++ b/src/app/_not-found/page.tsx @@ -0,0 +1,7 @@ +import { notFound } from 'next/navigation' + +// This page handles internal /_not-found redirects from Express middleware +export default function InternalNotFound() { + // This will trigger Next.js to render the not-found.tsx page + notFound() +} diff --git a/src/app/client-layout.tsx b/src/app/client-layout.tsx new file mode 100644 index 000000000000..60c80ec8c98a --- /dev/null +++ b/src/app/client-layout.tsx @@ -0,0 +1,126 @@ +'use client' + +import { ThemeProvider } from '@primer/react' +import { useEffect, useMemo, useState } from 'react' + +import { LocaleProvider } from '@/app/lib/locale-context' +import { useDetectLocale } from '@/app/lib/use-detect-locale' +import { useTheme } from '@/color-schemes/components/useTheme' +import { initializeEvents } from '@/events/components/events' +import { CTAPopoverProvider } from '@/frame/components/context/CTAContext' +import { SharedUIContextProvider } from '@/frame/components/context/SharedUIContext' +import { LanguagesContext, LanguagesContextT } from '@/languages/components/LanguagesContext' +import { clientLanguages, type ClientLanguageCode } from '@/languages/lib/client-languages' +import { MainContextProvider } from '@/app/components/MainContextProvider' +import { createMinimalMainContext } from '@/app/lib/main-context-adapter' +import type { AppRouterContext } from '@/app/lib/app-router-context' + +interface ClientLayoutProps { + readonly children: React.ReactNode + readonly appContext?: AppRouterContext + readonly pageData?: { + title?: string + fullTitle?: string + introPlainText?: string + topics?: string[] + documentType?: string + type?: string + hidden?: boolean + } +} + +export function ClientLayout({ children, appContext, pageData }: ClientLayoutProps): JSX.Element { + const { theme } = useTheme() + const locale: ClientLanguageCode = useDetectLocale() + const [isInitialized, setIsInitialized] = useState(false) + const [initializationError, setInitializationError] = useState(null) + + const languagesContext: LanguagesContextT = useMemo( + () => ({ + languages: clientLanguages, + }), + [], + ) + + // Create MainContext-compatible data for App Router + const mainContext = useMemo( + () => createMinimalMainContext(pageData, appContext), + [pageData, appContext], + ) + + useEffect(() => { + const initializeTheme = async (): Promise => { + try { + const html = document.documentElement + + if (theme.css?.colorMode) { + html.setAttribute('data-color-mode', theme.css.colorMode) + } + + if (theme.css?.darkTheme) { + html.setAttribute('data-dark-theme', theme.css.darkTheme) + } + + if (theme.css?.lightTheme) { + html.setAttribute('data-light-theme', theme.css.lightTheme) + } + + if (!isInitialized) { + await initializeEvents() + setIsInitialized(true) + } + } catch (error) { + console.error('Failed to initialize theme:', error) + setInitializationError(error as Error) + } + } + + initializeTheme() + }, [theme, isInitialized]) + + if (initializationError) { + return ( +
+
+

Something went wrong

+

Please try refreshing the page.

+ +
+
+ ) + } + + return ( + + + + + + +
{children}
+
+
+
+
+
+
+ ) +} diff --git a/src/app/components/AppRouterMainContext.tsx b/src/app/components/AppRouterMainContext.tsx new file mode 100644 index 000000000000..74b440ff2094 --- /dev/null +++ b/src/app/components/AppRouterMainContext.tsx @@ -0,0 +1,52 @@ +'use client' +import type { AppRouterContext } from '@/app/lib/app-router-context' +import type { MainContextT } from '@/frame/components/context/MainContext' +import { adaptAppRouterContextToMainContext } from '@/app/lib/main-context-adapter' +import { createContext, ReactNode, useContext, useMemo } from 'react' + +export const AppRouterMainContext = createContext(null) + +// Provides MainContext-compatible data +export const AppRouterCompatMainContext = createContext(null) + +export function AppRouterMainContextProvider({ + children, + context, +}: { + children: ReactNode + context: AppRouterContext +}) { + // Create a MainContext-compatible version for existing components + const mainContextCompat = useMemo(() => adaptAppRouterContextToMainContext(context), [context]) + + return ( + + + {children} + + + ) +} + +export function useAppRouterMainContext(): AppRouterContext { + const context = useContext(AppRouterMainContext) + + if (!context) { + throw new Error('useAppRouterMainContext must be used within AppRouterMainContextProvider') + } + + return context +} + +// Hook for components that need MainContext compatibility +export function useAppRouterCompatMainContext(): MainContextT { + const context = useContext(AppRouterCompatMainContext) + + if (!context) { + throw new Error( + 'useAppRouterCompatMainContext must be used within AppRouterMainContextProvider', + ) + } + + return context +} diff --git a/src/app/components/MainContextProvider.tsx b/src/app/components/MainContextProvider.tsx new file mode 100644 index 000000000000..708e6e5889c4 --- /dev/null +++ b/src/app/components/MainContextProvider.tsx @@ -0,0 +1,17 @@ +'use client' + +import type { ReactNode } from 'react' +import { MainContext, type MainContextT } from '@/frame/components/context/MainContext' + +interface MainContextProviderProps { + children: ReactNode + value: MainContextT +} + +/** + * App Router compatible MainContext provider + * This allows reusing existing components that depend on MainContext + */ +export function MainContextProvider({ children, value }: MainContextProviderProps) { + return {children} +} diff --git a/src/app/components/NotFoundContent.tsx b/src/app/components/NotFoundContent.tsx new file mode 100644 index 000000000000..e70a8385c650 --- /dev/null +++ b/src/app/components/NotFoundContent.tsx @@ -0,0 +1,119 @@ +'use client' +import { useAppRouterMainContext } from '@/app/components/AppRouterMainContext' +import { createTranslationFunctions } from '@/languages/lib/translation-utils' +import { CommentDiscussionIcon, MarkGithubIcon } from '@primer/octicons-react' +import { useMemo } from 'react' + +function SimpleHeader() { + const context = useAppRouterMainContext() + + const { t } = useMemo( + () => createTranslationFunctions(context.site.data.ui, ['header']), + [context.site.data.ui], + ) + + const siteTitle = t('github_docs') + + return ( + + ) +} + +function SimpleFooter() { + return ( + + ) +} + +function SimpleLead({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +export function NotFoundContent() { + const context = useAppRouterMainContext() + + const { t } = useMemo( + () => createTranslationFunctions(context.site.data.ui, ['meta']), + [context.site.data.ui], + ) + + return ( +
+ + +
+
+

{t('oops')}

+ It looks like this page doesn't exist. +

+ We track these errors automatically, but if the problem persists please feel free to + contact us. +

+ + + Contact support + +
+
+ + +
+ ) +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 000000000000..29fe4be37928 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,54 @@ +import '@/frame/stylesheets/index.scss' +import type { Metadata, Viewport } from 'next' +import { ReactNode } from 'react' + +export const metadata: Metadata = { + title: { + template: '%s | GitHub Docs', + default: 'GitHub Docs', + }, + icons: { + icon: [ + { url: '/assets/cb-345/images/site/favicon.png', sizes: '32x32', type: 'image/png' }, + { url: '/assets/cb-345/images/site/favicon.ico', sizes: '48x48', type: 'image/x-icon' }, + ], + shortcut: '/assets/cb-345/images/site/favicon.ico', + apple: '/assets/cb-345/images/site/favicon.png', + }, + verification: { + google: [ + 'OgdQc0GZfjDI52wDv1bkMT-SLpBUo_h5nn9mI9L22xQ', + 'c1kuD-K2HIVF635lypcsWPoD4kilo5-jA_wBFyT4uMY', + ], + }, +} + +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, +} + +interface RootLayoutProps { + readonly children: ReactNode +} + +// Root layout for App Router pages +export default function RootLayout({ children }: RootLayoutProps): JSX.Element { + return ( + + + + {/* DNS prefetch for performance */} + + + + {children} + + ) +} diff --git a/src/app/lib/app-router-context.ts b/src/app/lib/app-router-context.ts new file mode 100644 index 000000000000..800e1f198669 --- /dev/null +++ b/src/app/lib/app-router-context.ts @@ -0,0 +1,87 @@ +import { headers } from 'next/headers' +import { translate } from '@/languages/lib/translation-utils' +import { clientLanguageKeys } from '@/languages/lib/client-languages' +import { getUIDataMerged } from '@/data-directory/lib/get-data' + +export interface AppRouterContext { + currentLanguage: string + currentVersion: string + sitename: string + site: { + data: { + ui: any + } + } +} + +export async function getAppRouterContext(): Promise { + const headersList = await headers() + + // Get language and version from headers or fallbacks + const language = + headersList.get('x-docs-language') || + extractLanguageFromHeader(headersList.get('accept-language') || 'en') + const version = headersList.get('x-docs-version') || 'free-pro-team@latest' + + // Load UI data directly from data directory the same way as Pages Router does it + const uiData = getUIDataMerged(language) + + const siteName = translate(uiData, 'header.github_docs', 'GitHub Docs') + + return { + currentLanguage: language, + currentVersion: version, + sitename: siteName, + site: { + data: { + ui: uiData, + }, + }, + } +} + +function extractLanguageFromHeader(acceptLanguage: string): string { + // Fastly's custom VCL forces Accept-Language header to contain + // one of our supported language codes, so complex parsing isn't needed + const language = acceptLanguage.trim() + return clientLanguageKeys.includes(language) ? language : 'en' +} + +// Helper to create minimal MainContext-compatible object +export function createAppRouterMainContext(appContext: AppRouterContext): any { + return { + currentLanguage: appContext.currentLanguage, + currentVersion: appContext.currentVersion, + data: { + ui: appContext.site.data.ui, + reusables: {}, + variables: { + release_candidate: { version: null }, + }, + }, + allVersions: {}, + breadcrumbs: {}, + communityRedirect: {}, + currentPathWithoutLanguage: '', + currentProduct: null, + currentProductName: '', + currentProductTree: null, + enterpriseServerReleases: { + isOldestReleaseDeprecated: false, + oldestSupported: '', + nextDeprecationDate: '', + supported: [], + }, + enterpriseServerVersions: [], + error: '', + featureFlags: {}, + fullUrl: '', + isHomepageVersion: false, + nonEnterpriseDefaultVersion: 'free-pro-team@latest', + page: null, + relativePath: null, + sidebarTree: null, + status: 404, + xHost: '', + } +} diff --git a/src/app/lib/locale-context.tsx b/src/app/lib/locale-context.tsx new file mode 100644 index 000000000000..7840c8af714e --- /dev/null +++ b/src/app/lib/locale-context.tsx @@ -0,0 +1,121 @@ +'use client' + +import { createContext, useContext, ReactNode, useMemo } from 'react' +import { + clientLanguages, + clientLanguageKeys, + type ClientLanguageCode, +} from '@/languages/lib/client-languages' + +interface LocaleContextType { + readonly locale: ClientLanguageCode + readonly isValidLocale: (locale: string) => locale is ClientLanguageCode + readonly getSupportedLocales: () => readonly ClientLanguageCode[] + readonly getLocaleDisplayName: (locale: ClientLanguageCode) => string + readonly getLocaleNativeName: (locale: ClientLanguageCode) => string +} + +const LocaleContext = createContext(null) + +interface LocaleProviderProps { + readonly children: ReactNode + readonly locale: ClientLanguageCode +} + +// Use client languages as the source of truth for supported locales +const SUPPORTED_LOCALES: readonly ClientLanguageCode[] = clientLanguageKeys as ClientLanguageCode[] + +/** + * Validates if a string is a supported locale + */ +function isValidLocale(locale: string): locale is ClientLanguageCode { + return clientLanguageKeys.includes(locale) +} + +/** + * Gets display name for a locale from client languages data + */ +function getLocaleDisplayName(locale: ClientLanguageCode): string { + return clientLanguages[locale]?.name || locale +} + +/** + * Gets native name for a locale from client languages data + */ +function getLocaleNativeName(locale: ClientLanguageCode): string { + return clientLanguages[locale]?.nativeName || clientLanguages[locale]?.name || locale +} + +/** + * Gets browser language preference as a valid locale + */ +function getBrowserLocale(): ClientLanguageCode { + if (typeof window === 'undefined') return 'en' + + const browserLang = window.navigator.language.split('-')[0] + return isValidLocale(browserLang) ? browserLang : 'en' +} + +/** + * Enhanced locale provider with validation and error handling + */ +export function LocaleProvider({ children, locale }: LocaleProviderProps): JSX.Element { + const contextValue = useMemo( + () => ({ + locale: isValidLocale(locale) ? locale : 'en', + isValidLocale, + getSupportedLocales: () => SUPPORTED_LOCALES, + getLocaleDisplayName, + getLocaleNativeName, + }), + [locale], + ) + + return {children} +} + +/** + * Hook to get current locale with enhanced error handling + */ +export function useLocale(): ClientLanguageCode { + const context = useContext(LocaleContext) + + if (context) { + return context.locale + } + + // Fallback for when not within LocaleProvider + // This handles cases where the hook is used outside of the provider + console.warn('useLocale called outside of LocaleProvider, using fallback') + return getBrowserLocale() +} + +/** + * Hook to validate locales + */ +export function useLocaleValidation() { + const context = useContext(LocaleContext) + + return { + isValidLocale: context?.isValidLocale ?? isValidLocale, + getSupportedLocales: context?.getSupportedLocales ?? (() => SUPPORTED_LOCALES), + getLocaleDisplayName: context?.getLocaleDisplayName ?? getLocaleDisplayName, + getLocaleNativeName: context?.getLocaleNativeName ?? getLocaleNativeName, + } +} + +/** + * Hook to get locale context (for advanced use cases) + */ +export function useLocaleContext(): LocaleContextType { + const context = useContext(LocaleContext) + + if (!context) { + throw new Error('useLocaleContext must be used within a LocaleProvider') + } + + return context +} + +export { isValidLocale, getLocaleDisplayName, getLocaleNativeName } +export type { LocaleContextType, ClientLanguageCode } diff --git a/src/app/lib/main-context-adapter.ts b/src/app/lib/main-context-adapter.ts new file mode 100644 index 000000000000..ee79b2b62693 --- /dev/null +++ b/src/app/lib/main-context-adapter.ts @@ -0,0 +1,108 @@ +import type { MainContextT } from '@/frame/components/context/MainContext' +import type { AppRouterContext } from '@/app/lib/app-router-context' + +/** + * Adapter to create MainContext-compatible data from App Router context + * Allows reusing existing components that depend on MainContext + */ +export function adaptAppRouterContextToMainContext( + appContext: AppRouterContext, + overrides: Partial = {}, +): MainContextT { + const baseContext: MainContextT = { + data: { + ui: appContext.site.data.ui, + reusables: {}, + variables: { + release_candidate: { version: null }, + }, + }, + + // Default/fallback values that can be overridden + allVersions: {}, + breadcrumbs: { + product: { + title: '', + href: undefined, + }, + }, + communityRedirect: { + name: '', + href: '', + }, + currentPathWithoutLanguage: '', + currentProduct: undefined, + currentProductName: '', + currentProductTree: null, + currentVersion: appContext.currentVersion, + enterpriseServerReleases: { + isOldestReleaseDeprecated: false, + oldestSupported: '', + nextDeprecationDate: '', + supported: [], + }, + enterpriseServerVersions: [], + error: '', + featureFlags: {}, + fullUrl: '', + isHomepageVersion: false, + nonEnterpriseDefaultVersion: 'free-pro-team@latest', + page: null, + relativePath: undefined, + sidebarTree: null, + status: 200, + xHost: '', + + // Apply any overrides + ...overrides, + } + + return baseContext +} + +/** + * For components that need MainContext data in App Router, + * this helper provides a minimal compatible context + */ +export function createMinimalMainContext( + pageData?: { + title?: string + fullTitle?: string + introPlainText?: string + topics?: string[] + documentType?: string + type?: string + hidden?: boolean + }, + appContext?: AppRouterContext, +): MainContextT { + const defaultAppContext: AppRouterContext = appContext || { + currentLanguage: 'en', + currentVersion: 'free-pro-team@latest', + sitename: 'GitHub Docs', + site: { + data: { + ui: { + header: { github_docs: 'GitHub Docs' }, + footer: {}, + }, + }, + }, + } + + return adaptAppRouterContextToMainContext(defaultAppContext, { + page: pageData + ? { + documentType: pageData.documentType || 'article', + type: pageData.type, + topics: pageData.topics || [], + title: pageData.title || 'Page Not Found', + fullTitle: pageData.fullTitle || pageData.title, + introPlainText: pageData.introPlainText, + hidden: pageData.hidden || false, + noEarlyAccessBanner: false, + applicableVersions: [], + } + : null, + }) +} diff --git a/src/app/lib/routing-patterns.ts b/src/app/lib/routing-patterns.ts new file mode 100644 index 000000000000..b006abf66243 --- /dev/null +++ b/src/app/lib/routing-patterns.ts @@ -0,0 +1,118 @@ +/** + * Shared routing patterns for determining App Router vs Pages Router routing decisions. + * This module centralizes pattern definitions to ensure consistency between + * app-router-gateway.ts and render-page.ts + */ + +// Define which routes should use App Router (without locale prefix) +const APP_ROUTER_ROUTES = new Set([ + '/_not-found', + '/404', + // Add more routes as you migrate them +]) + +/** + * Determines if a given path should be handled by the App Router + * @param path - The request path to check + * @param pageFound - Whether a page was found by the findPage middleware + * @returns true if the path should use App Router, false for Pages Router + */ +export function shouldUseAppRouter(path: string, pageFound: boolean = true): boolean { + // Strip locale prefix before checking + const strippedPath = stripLocalePrefix(path) + + // Check exact matches on the stripped path for specific App Router routes + if (APP_ROUTER_ROUTES.has(strippedPath)) { + return true + } + + // Special case: paths ending with /empty-categories should always 404 via App Router + // This handles translation tests where certain versioned paths should not exist + if (strippedPath.endsWith('/empty-categories')) { + return true + } + + // For 404 migration: If no page was found, use App Router for 404 handling + if (!pageFound) { + return true + } + + return false +} + +/** + * Checks if a path looks like a "junk path" that should be handled by shielding middleware + * These are paths like WordPress attacks, config files, etc. that need specific 404 handling + */ +export function isJunkPath(path: string): boolean { + // Common attack patterns and junk paths that should be handled by shielding + const junkPatterns = [ + /^\/wp-/, // WordPress paths: /wp-content, /wp-login.php, etc. + /^\/[^/]*\.php$/, // PHP files in root: xmlrpc.php, wp-login.php (but not /en/delicious-snacks/donuts.php) + /^\/~\w+/, // User directory paths: /~root, /~admin, etc. + /\/\.env/, // Environment files: /.env, /.env.local, etc. + /\/package(-lock)?\.json$/, // Node.js package files + /^\/_{2,}/, // Multiple underscores: ///, /\\, etc. + /^\/\\+/, // Backslash attacks + ] + + return junkPatterns.some((pattern) => pattern.test(path)) +} + +/** + * Checks if a path contains a version identifier (e.g., enterprise-server@3.16, enterprise-cloud@latest) + * This helps distinguish versioned docs paths from regular paths that should potentially use App Router + */ +export function isVersionedPath(path: string): boolean { + const strippedPath = stripLocalePrefix(path) + + // Check for version patterns: plan@release + // Examples: enterprise-server@3.16, enterprise-server@latest, enterprise-cloud@latest, free-pro-team@latest + const versionPattern = + /(enterprise-server@[\d.]+|enterprise-server@latest|enterprise-cloud@latest|free-pro-team@latest)/ + return versionPattern.test(strippedPath) +} + +/** + * Checks if a versioned path contains invalid segments that should result in 404 + * These should be routed to App Router for proper 404 handling + */ +export function isInvalidVersionedPath(path: string): boolean { + const strippedPath = stripLocalePrefix(path) + + // Check for obviously invalid paths that should 404 + // Example: /enterprise-server@latest/ANY/admin or /enterprise-server@12345/anything + return ( + strippedPath.includes('/ANY/') || + /enterprise-server@12345/.test(strippedPath) || + // Add other invalid patterns as needed + false + ) +} + +/** + * Decodes a URL path, handling URL-encoded characters like %40 -> @ + */ +export function decodePathSafely(path: string): string { + try { + return decodeURIComponent(path) + } catch { + // If decoding fails, use original path + return path + } +} + +/** + * Strips the locale prefix from the path if present + * e.g., /en/search -> /search + * e.g., /en/enterprise-server@3.17 -> /enterprise-server@3.17 + */ +export function stripLocalePrefix(path: string): string { + const decodedPath = decodePathSafely(path) + + const localeMatch = decodedPath.match(/^\/([a-z]{2})(\/.*)?$/) + if (localeMatch) { + return localeMatch[2] || '/' + } + return decodedPath +} diff --git a/src/app/lib/use-detect-locale.tsx b/src/app/lib/use-detect-locale.tsx new file mode 100644 index 000000000000..410c6ca026fe --- /dev/null +++ b/src/app/lib/use-detect-locale.tsx @@ -0,0 +1,61 @@ +'use client' + +import { usePathname } from 'next/navigation' +import { useMemo } from 'react' +import { clientLanguageKeys, type ClientLanguageCode } from '@/languages/lib/client-languages' + +/** + * Validates if a string is a supported locale using client languages + */ +function isValidLocale(locale: string): locale is ClientLanguageCode { + return clientLanguageKeys.includes(locale) +} + +/** + * Hook to detect locale from various sources with fallback logic + */ +export function useDetectLocale(): ClientLanguageCode { + const pathname = usePathname() + + return useMemo(() => { + // Extract locale from pathname (e.g., /es/search -> 'es') + if (pathname) { + const pathSegments = pathname.split('/') + const firstSegment = pathSegments[1] + + if (firstSegment && isValidLocale(firstSegment)) { + return firstSegment + } + } + + // Fallback to browser locale if available + if (typeof window !== 'undefined' && window.navigator?.language) { + const browserLocale = window.navigator.language.split('-')[0] + if (isValidLocale(browserLocale)) { + return browserLocale + } + } + + return 'en' + }, [pathname]) +} + +/** + * Utility function to detect locale from pathname (for server-side use) + */ +export function detectLocaleFromPath(pathname: string): ClientLanguageCode { + const pathSegments = pathname.split('/') + const firstSegment = pathSegments[1] + + if (firstSegment && isValidLocale(firstSegment)) { + return firstSegment + } + + return 'en' +} + +export function getSupportedLocales(): readonly ClientLanguageCode[] { + return clientLanguageKeys as ClientLanguageCode[] +} + +export { isValidLocale } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 000000000000..0af316061fc9 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,27 @@ +import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext' +import { NotFoundContent } from '@/app/components/NotFoundContent' +import { getAppRouterContext } from '@/app/lib/app-router-context' +import type { Metadata } from 'next' + +// Force this page to be dynamic so it can access headers() +export const dynamic = 'force-dynamic' + +export const metadata: Metadata = { + title: '404 - Page not found', + other: { + status: '404', + }, +} + +async function NotFoundPage() { + // Get context from headers set by gateway middleware + const appContext = await getAppRouterContext() + + return ( + + + + ) +} + +export default NotFoundPage diff --git a/src/app/types.ts b/src/app/types.ts new file mode 100644 index 000000000000..c86401e08c43 --- /dev/null +++ b/src/app/types.ts @@ -0,0 +1,83 @@ +/** + * Enhanced type definitions for the app router with strict validation + */ + +import type { ClientLanguageCode } from '@/languages/lib/client-languages' + +// Core theme types with strict validation +export type Theme = 'light' | 'dark' | 'auto' +export type ColorMode = 'light' | 'dark' + +// Re-export locale type from client-languages for consistency +export type Locale = ClientLanguageCode + +// Version and product identifiers with validation +export type VersionId = string +export type ProductId = string +export type CategoryId = string + +// Enhanced page parameters with validation +export interface PageParams { + readonly versionId?: VersionId + readonly productId?: ProductId + readonly categoryId?: CategoryId +} + +// Search parameters with better typing +export interface SearchParams { + readonly [key: string]: string | string[] | undefined +} + +// Route context with comprehensive typing +export interface RouteContext { + readonly locale: Locale + readonly versionId?: VersionId + readonly productId?: ProductId + readonly categoryId?: CategoryId +} + +// Theme configuration with complete typing +export interface ThemeConfig { + readonly theme: Theme + readonly colorMode: ColorMode + readonly component: { + readonly colorMode: ColorMode + readonly dayScheme: string + readonly nightScheme: string + } +} + +// Error types for better error handling +export interface AppError { + readonly message: string + readonly code: string + readonly statusCode: number + readonly context?: Record +} + +// Navigation item with accessibility support +export interface NavigationItem { + readonly href: string + readonly title: string + readonly isActive?: boolean + readonly ariaLabel?: string + readonly children?: readonly NavigationItem[] +} + +// Layout props with enhanced typing +export interface LayoutProps { + readonly children: React.ReactNode + readonly className?: string +} + +// Component props with better composition +export interface ComponentProps { + readonly className?: string + readonly children?: React.ReactNode + readonly 'data-testid'?: string +} + +// Utility types for better type safety +export type NonEmptyArray = [T, ...T[]] +export type RequiredFields = T & Required> +export type OptionalFields = Omit & Partial> diff --git a/src/app/validators.ts b/src/app/validators.ts new file mode 100644 index 000000000000..eaf42c0031ca --- /dev/null +++ b/src/app/validators.ts @@ -0,0 +1,16 @@ +import { isValidLocale } from '@/app/lib/use-detect-locale' +import { PageParams, SearchParams, Theme } from './types' + +// Runtime validation helpers (theme-specific only, locale validation moved to use-detect-locale) +export const isValidTheme = (theme: string): theme is Theme => + ['light', 'dark', 'auto'].includes(theme) + +// Type guards for runtime validation +export const isPageParams = (obj: unknown): obj is PageParams => + typeof obj === 'object' && obj !== null + +export const isSearchParams = (obj: unknown): obj is SearchParams => + typeof obj === 'object' && obj !== null + +// Re-export locale validation +export { isValidLocale } diff --git a/src/frame/lib/header-utils.ts b/src/frame/lib/header-utils.ts new file mode 100644 index 000000000000..5ac2d7cc000e --- /dev/null +++ b/src/frame/lib/header-utils.ts @@ -0,0 +1,84 @@ +/** + * Utilities for safely serializing data for HTTP headers + */ + +export interface MinimalUIData { + header: { + github_docs?: string + } + footer?: any +} + +/** + * Safely serialize data to a base64-encoded JSON string for HTTP headers + * Handle encoding issues and provide fallbacks for serialization errors + */ +export function safeStringifyForHeader(data: any): string { + try { + const jsonString = JSON.stringify(data) + // Encode to base64 to avoid header character issues + return Buffer.from(jsonString, 'utf8').toString('base64') + } catch (e) { + console.warn('Failed to stringify data for header:', e) + // Return minimal fallback as base64 + const fallback = JSON.stringify({ + header: { github_docs: 'GitHub Docs' }, + footer: {}, + }) + return Buffer.from(fallback, 'utf8').toString('base64') + } +} + +/** + * Create minimal UI data from Express context + * Provide consistent fallbacks for missing data + */ +export function createMinimalUIData(context?: any): MinimalUIData { + if (!context?.site?.data?.ui) { + return { + header: { github_docs: 'GitHub Docs' }, + footer: {}, + } + } + + return { + header: context.site.data.ui.header || { github_docs: 'GitHub Docs' }, + footer: context.site.data.ui.footer || {}, + } +} + +/** + * Set context headers for App Router from Express context + * Preserve headers from Fastly + */ +export function setAppRouterContextHeaders( + req: any, + res: any, + preserveFastlyHeaders: boolean = true, +): void { + if (req.context) { + // Only set language header if Fastly hasn't already set it (or if not preserving) + if (!preserveFastlyHeaders || !req.headers['x-docs-language']) { + res.setHeader('x-docs-language', req.context.currentLanguage || 'en') + } + + // Only set version header if Fastly hasn't already set it (or if not preserving) + if (!preserveFastlyHeaders || !req.headers['x-docs-version']) { + res.setHeader('x-docs-version', req.context.currentVersion || 'free-pro-team@latest') + } + + const minimalUI = createMinimalUIData(req.context) + res.setHeader('x-docs-ui-data', safeStringifyForHeader(minimalUI)) + } else { + // Fallback when no Express context is available + res.setHeader('x-docs-language', 'en') + res.setHeader('x-docs-version', 'free-pro-team@latest') + res.setHeader( + 'x-docs-ui-data', + safeStringifyForHeader({ + header: { github_docs: 'GitHub Docs' }, + footer: {}, + }), + ) + } +} diff --git a/src/frame/middleware/app-router-gateway.ts b/src/frame/middleware/app-router-gateway.ts new file mode 100644 index 000000000000..695a141942bb --- /dev/null +++ b/src/frame/middleware/app-router-gateway.ts @@ -0,0 +1,114 @@ +import { + isInvalidVersionedPath, + isJunkPath, + isVersionedPath, + shouldUseAppRouter, + stripLocalePrefix, +} from '@/app/lib/routing-patterns' +import type { ExtendedRequest } from '@/types' +import type { NextFunction, Response } from 'express' +import { setAppRouterContextHeaders } from '../lib/header-utils' +import { defaultCacheControl } from './cache-control' +import { nextApp } from './next' + +export default function appRouterGateway(req: ExtendedRequest, res: Response, next: NextFunction) { + const path = req.path || req.url + const strippedPath = stripLocalePrefix(path) + + // Only intercept GET and HEAD requests, and prioritize /empty-categories paths + if (req.method !== 'GET' && req.method !== 'HEAD') { + return next() + } + + // Special case: Always intercept /empty-categories paths regardless of method + if (strippedPath.endsWith('/empty-categories')) { + // Skip the normal exclusion logic and go straight to App Router routing + const pageFound = !!(req.context && req.context.page) + + if (shouldUseAppRouter(path, pageFound)) { + // Set the URL to trigger App Router's not-found.tsx since /empty-categories should 404 + req.url = '/404' + res.status(404) + defaultCacheControl(res) + + // Set context headers for App Router (don't preserve Fastly since this is internal routing) + setAppRouterContextHeaders(req, res, false) + + res.locals = res.locals || {} + res.locals.handledByAppRouter = true + return nextApp.getRequestHandler()(req, res) + } + } + + // Don't route static assets, API routes, valid versioned docs paths, or junk paths to App Router + // Let them be handled by Pages Router middleware (shielding, API handlers, etc.) + // However, invalid versioned paths (like paths with /ANY/ or bogus versions) should go to App Router for 404 + // EXCEPTION: /empty-categories paths should always go to App Router for proper 404 handling + const strippedPathForExclusion = stripLocalePrefix(path) + + if ( + path.startsWith('/_next/') || + path.startsWith('/assets/') || + path.startsWith('/public/') || + path.startsWith('/api/') || + path === '/api' || + isJunkPath(path) || + (isVersionedPath(path) && + !isInvalidVersionedPath(path) && + !strippedPathForExclusion.endsWith('/empty-categories')) || + path.includes('.css') || + path.includes('.js') || + path.includes('.map') || + path.includes('.ico') || + path.includes('.png') || + path.includes('.svg') || + path.endsWith('/manifest.json') || + path.endsWith('/robots.txt') || + path.endsWith('/llms.txt') || + path.endsWith('/_500') + ) { + return next() + } + + // Check if a page was found by the findPage middleware + const pageFound = !!(req.context && req.context.page) + + if (shouldUseAppRouter(path, pageFound)) { + console.log(`[INFO] Using App Router for path: ${path} (pageFound: ${!!pageFound})`) + + // Strip locale prefix for App Router routing + const strippedPath = stripLocalePrefix(path) + + // For 404 routes (either explicit or missing pages), always route to our 404 page + if (strippedPath === '/404' || strippedPath === '/_not-found' || !pageFound) { + // Set the URL to trigger App Router's not-found.tsx + req.url = '/404' // Use a real App Router page route + res.status(404) + + // Set proper cache headers for 404 responses to match Pages Router behavior + defaultCacheControl(res) + } else { + // For other App Router routes, use the stripped path + const originalUrl = req.url + req.url = strippedPath + originalUrl.substring(req.path.length) + } + + // Set context headers for App Router (preserve Fastly headers) + setAppRouterContextHeaders(req, res, true) + + // Use Next.js App Router to handle this request + // The App Router will use the appropriate page.tsx or not-found.tsx + // IMPORTANT: Don't call next() - this terminates the Express middleware chain + + // Mark response as handled to prevent further middleware processing + res.locals = res.locals || {} + res.locals.handledByAppRouter = true + + // Use the Next.js request handler and DO NOT call next() + return nextApp.getRequestHandler()(req, res) + } + + console.log(`[INFO] Using Pages Router for path: ${path}`) + // Continue with Pages Router pipeline + return next() +} diff --git a/src/frame/middleware/helmet.ts b/src/frame/middleware/helmet.ts index 10a44572d919..9cf66d2c6902 100644 --- a/src/frame/middleware/helmet.ts +++ b/src/frame/middleware/helmet.ts @@ -1,8 +1,9 @@ -import type { NextFunction, Request, Response } from 'express' -import helmet from 'helmet' +import { shouldUseAppRouter, isVersionedPath } from '@/app/lib/routing-patterns' import { isArchivedVersion } from '@/archives/lib/is-archived-version' -import versionSatisfiesRange from '@/versions/lib/version-satisfies-range' import { languagePrefixPathRegex } from '@/languages/lib/languages' +import versionSatisfiesRange from '@/versions/lib/version-satisfies-range' +import type { NextFunction, Request, Response } from 'express' +import helmet from 'helmet' const isDev = process.env.NODE_ENV === 'development' const GITHUB_DOMAINS = [ @@ -80,10 +81,16 @@ devDirs.scriptSrcAttr.push("'unsafe-inline'") const STATIC_DEPRECATED_OPTIONS = structuredClone(DEFAULT_OPTIONS) STATIC_DEPRECATED_OPTIONS.contentSecurityPolicy.directives.scriptSrc.push("'unsafe-inline'") +// App Router specific CSP that allows inline scripts for NextJS hydration +const APP_ROUTER_OPTIONS = structuredClone(DEFAULT_OPTIONS) +const appRouterDirs = APP_ROUTER_OPTIONS.contentSecurityPolicy.directives +appRouterDirs.scriptSrc.push("'unsafe-inline'") // Required for NextJS App Router hydration + const defaultHelmet = helmet(DEFAULT_OPTIONS) const nodeDeprecatedHelmet = helmet(NODE_DEPRECATED_OPTIONS) const staticDeprecatedHelmet = helmet(STATIC_DEPRECATED_OPTIONS) const developerDeprecatedHelmet = helmet(DEVELOPER_DEPRECATED_OPTIONS) +const appRouterHelmet = helmet(APP_ROUTER_OPTIONS) export default function helmetMiddleware(req: Request, res: Response, next: NextFunction) { // Enable CORS @@ -91,6 +98,24 @@ export default function helmetMiddleware(req: Request, res: Response, next: Next res.set('access-control-allow-origin', '*') } + // Check if this is an explicit App Router route + if (shouldUseAppRouter(req.path, true)) { + return appRouterHelmet(req, res, next) + } + + // For potential 404s that might be handled by App Router, use App Router CSP + // This is a safe fallback since App Router CSP includes all necessary permissions + // Apply to any path that could potentially be a 404, regardless of locale prefix + if ( + !req.path.startsWith('/_next/') && + !req.path.startsWith('/assets/') && + !req.path.startsWith('/api/') && + !isVersionedPath(req.path) + ) { + // This might be a 404 that gets routed to App Router, so use App Router CSP + return appRouterHelmet(req, res, next) + } + // Determine version for exceptions const { requestedVersion } = isArchivedVersion(req) diff --git a/src/frame/middleware/index.ts b/src/frame/middleware/index.ts index 7695344ec79c..8f6afe911897 100644 --- a/src/frame/middleware/index.ts +++ b/src/frame/middleware/index.ts @@ -65,6 +65,7 @@ import shielding from '@/shielding/middleware' import { MAX_REQUEST_TIMEOUT } from '@/frame/lib/constants' import { initLoggerContext } from '@/observability/logger/lib/logger-context' import { getAutomaticRequestLogger } from '@/observability/logger/middleware/get-automatic-request-logger' +import appRouterGateway from './app-router-gateway' const { NODE_ENV } = process.env const isTest = NODE_ENV === 'test' || process.env.GITHUB_ACTIONS === 'true' @@ -221,6 +222,9 @@ export default function (app: Express) { // Check for a dropped connection before proceeding app.use(haltOnDroppedConnection) + // *** Add App Router Gateway here - before heavy contextualizers *** + app.use(asyncMiddleware(appRouterGateway)) + // *** Rendering, 2xx responses *** app.use('/api', api) app.use('/llms.txt', llmsTxt) diff --git a/src/frame/middleware/render-page.ts b/src/frame/middleware/render-page.ts index 50fe0b330d7f..c5fe1e7c3800 100644 --- a/src/frame/middleware/render-page.ts +++ b/src/frame/middleware/render-page.ts @@ -1,20 +1,20 @@ -import http from 'http' - -import { get } from 'lodash-es' import type { Response } from 'express' + import type { Failbot } from '@github/failbot' +import { get } from 'lodash-es' -import type { ExtendedRequest } from '@/types' -import FailBot from '@/observability/lib/failbot' -import patterns from '@/frame/lib/patterns' import getMiniTocItems from '@/frame/lib/get-mini-toc-items' +import patterns from '@/frame/lib/patterns' import { pathLanguagePrefixed } from '@/languages/lib/languages' +import FailBot from '@/observability/lib/failbot' import statsd from '@/observability/lib/statsd' +import type { ExtendedRequest } from '@/types' import { allVersions } from '@/versions/lib/all-versions' +import { minimumNotFoundHtml } from '../lib/constants' +import { setAppRouterContextHeaders } from '../lib/header-utils' +import { defaultCacheControl } from './cache-control' import { isConnectionDropped } from './halt-on-dropped-connection' import { nextHandleRequest } from './next' -import { defaultCacheControl } from './cache-control' -import { minimumNotFoundHtml } from '../lib/constants' const STATSD_KEY_RENDER = 'middleware.render_page' const STATSD_KEY_404 = 'middleware.render_404' @@ -45,6 +45,11 @@ async function buildMiniTocItems(req: ExtendedRequest): Promise) => { const { data } = useMainContext() - const loadedData = data.ui - const namespacesArray = Array.isArray(namespaces) ? namespaces : [namespaces] - - for (const namespace of namespacesArray) { - if (!(namespace in loadedData)) { - console.warn( - 'The following namespaces in data.ui have been loaded: ' + - JSON.stringify(Object.keys(loadedData).sort()), - ) - throw new TranslationNamespaceError( - `Namespace "${namespace}" not found in data. ` + - 'Follow the stack trace to see which useTranslation(...) call is ' + - 'causing this error. If the namespace is present in data/ui.yml ' + - 'but this error is happening, find the related component ' + - 'getServerSideProps() it goes through and make sure it calls ' + - `addUINamespaces() with "${namespace}".`, - ) - } - } - - function carefulGetWrapper(path: string) { - for (const namespace of namespacesArray) { - if (!(namespace in loadedData)) { - throw new TranslationNamespaceError(`Namespace "${namespace}" not found in data. `) - } - const deeper = loadedData[namespace] - if (typeof deeper === 'string') { - continue - } - try { - return carefulGet(deeper, path) - } catch (error) { - if (!(error instanceof UngettableError)) { - throw error - } - } - } - - return carefulGet(loadedData, path) - } - - return { - tObject: (strings: TemplateStringsArray | string) => { - const key = typeof strings === 'string' ? strings : String.raw(strings) - return carefulGetWrapper(key) as UIStrings - }, - t: (strings: TemplateStringsArray | string, ...values: Array) => { - const key = typeof strings === 'string' ? strings : String.raw(strings, ...values) - return carefulGetWrapper(key) as string - }, - } + return createTranslationFunctions(loadedData, namespaces) } -function carefulGet(uiData: UIStrings, dottedPath: string) { - const splitPath = dottedPath.split('.') - const start = splitPath[0] - if (!(start in uiData)) { - throw new UngettableError( - `Namespace "${start}" not found in loaded data (not one of: ${Object.keys(uiData).sort()})`, - ) - } - if (splitPath.length > 1) { - const deeper = uiData[start] - if (typeof deeper === 'string') { - throw new Error(`Namespace "${start}" is a string, not an object`) - } - return carefulGet(deeper, splitPath.slice(1).join('.')) - } else { - if (!(start in uiData)) { - throw new UngettableError(`Key "${start}" not found in loaded data`) - } - return uiData[start] - } +/** + * Hook for App Router contexts that don't use MainContext + */ +export const useAppTranslation = (uiData: UIStrings, namespaces: string | Array) => { + return createTranslationFunctions(uiData, namespaces) } diff --git a/src/languages/lib/client-languages.ts b/src/languages/lib/client-languages.ts new file mode 100644 index 000000000000..c238b09032eb --- /dev/null +++ b/src/languages/lib/client-languages.ts @@ -0,0 +1,67 @@ +import type { LanguageItem } from '@/languages/components/LanguagesContext' + +/** + * Client-safe language data extracted from src/languages/lib/languages.ts. + * Only used by frontend components. + * Does not include server-side logic or Node.js-specific fs or path operations. + */ +export const clientLanguages: Record = { + en: { + name: 'English', + code: 'en', + nativeName: 'English', + hreflang: 'en', + }, + es: { + name: 'Spanish', + code: 'es', + nativeName: 'Español', + hreflang: 'es', + }, + ja: { + name: 'Japanese', + code: 'ja', + nativeName: '日本語', + hreflang: 'ja', + }, + pt: { + name: 'Portuguese', + code: 'pt', + nativeName: 'Português do Brasil', + hreflang: 'pt', + }, + zh: { + name: 'Simplified Chinese', + code: 'zh', + nativeName: '简体中文', + hreflang: 'zh-Hans', + }, + ru: { + name: 'Russian', + code: 'ru', + nativeName: 'Русский', + hreflang: 'ru', + }, + fr: { + name: 'French', + code: 'fr', + nativeName: 'Français', + hreflang: 'fr', + }, + ko: { + name: 'Korean', + code: 'ko', + nativeName: '한국어', + hreflang: 'ko', + }, + de: { + name: 'German', + code: 'de', + nativeName: 'Deutsch', + hreflang: 'de', + }, +} + +export const clientLanguageKeys: string[] = Object.keys(clientLanguages) + +export type ClientLanguageCode = keyof typeof clientLanguages diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts index 1bff3afcd86b..fda0b9af99ae 100644 --- a/src/languages/lib/correct-translation-content.ts +++ b/src/languages/lib/correct-translation-content.ts @@ -74,6 +74,25 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% необработанные %}', '{% raw %}') content = content.replaceAll('{% подсказки %}', '{% tip %}') + // Fix YAML quote issues in UI files. Specifically the disclaimer href attribute + // href="...}> -> href="..."> + content = content.replace(/href="([^"]*)}>/g, 'href="$1">') + + // Fix double quotes in Russian YAML files that cause parsing errors + // ""https:// -> "https:// + content = content.replace(/href=""https:\/\//g, 'href="https://') + + // Fix empty HTML tags that cause YAML parsing issues + content = content.replaceAll('', '') + content = content.replaceAll('', '') + + // Fix specific Russian UI YAML issues causing 502 errors + // Remove empty bold tags from early_access notice + content = content.replace(/early_access:\s*"([^"]*)<\/b>([^"]*)"/, 'early_access: "$1$2"') + + // Remove empty underline tags from privacy disclaimer + content = content.replace(/(privacy_disclaimer:[^<]*)<\/u>/g, '$1') + // For the rather custom Russian translation of // the content/get-started/learning-about-github/github-glossary.md page // These string replacements speak for themselves. @@ -90,6 +109,13 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% データ variables', '{% data variables') content = content.replaceAll('{% データvariables', '{% data variables') + // Fix specific issue likely causing 502 errors + // Remove trailing quote from the problematic translation + content = content.replace( + /asked_too_many_times:\s*申し訳ありません。短い時間に質問が多すぎます。\s*しばらく待ってからもう一度やり直してください。"\s*$/gm, + 'asked_too_many_times: 申し訳ありません。短い時間に質問が多すぎます。 しばらく待ってからもう一度やり直してください。', + ) + // Internal issue #4160 content = content.replaceAll( '- % data variables.product.prodname_copilot_enterprise %}', diff --git a/src/languages/lib/translation-utils.ts b/src/languages/lib/translation-utils.ts new file mode 100644 index 000000000000..40acaa3b7369 --- /dev/null +++ b/src/languages/lib/translation-utils.ts @@ -0,0 +1,176 @@ +import type { UIStrings } from '@/frame/components/context/MainContext' + +class UngettableError extends Error {} + +/** + * Generic translation function that works with both client and server components + * With error handling for missing namespaces in CJK/Cyrillic languages + */ +export function createTranslationFunctions(uiData: UIStrings, namespaces: string | Array) { + const namespacesArray = Array.isArray(namespaces) ? namespaces : [namespaces] + + const missingNamespaces: string[] = [] + for (const namespace of namespacesArray) { + if (!(namespace in uiData)) { + missingNamespaces.push(namespace) + } + } + + if (missingNamespaces.length > 0) { + console.warn( + `Missing namespaces [${missingNamespaces.join(', ')}] in UI data. ` + + 'Available namespaces: ' + + Object.keys(uiData).sort().join(', '), + ) + + // For 404 pages, we can't afford to throw errors; create defensive fallbacks + if (missingNamespaces.includes('meta')) { + console.warn('Creating fallback meta namespace for 404 page rendering') + uiData = { + ...uiData, + meta: { + oops: 'Ooops!', + ...(typeof uiData.meta === 'object' && uiData.meta !== null ? uiData.meta : {}), + }, + } as UIStrings + } + + // Still missing critical namespaces? Create minimal fallbacks + for (const namespace of missingNamespaces) { + if (!(namespace in uiData)) { + uiData = { + ...uiData, + [namespace]: {} as UIStrings, + } as UIStrings + } + } + } + + function carefulGetWrapper(path: string, fallback?: string) { + try { + // Try each namespace in order + for (const namespace of namespacesArray) { + if (!(namespace in uiData)) { + continue // Skip missing namespaces + } + const deeper = uiData[namespace] + if (typeof deeper === 'string') { + continue + } + try { + return carefulGet(deeper, path) + } catch (error) { + if (!(error instanceof UngettableError)) { + console.warn(`Translation error in namespace "${namespace}" for path "${path}":`, error) + } + } + } + + // Fallback to searching the full UI data + return carefulGet(uiData, path) + } catch { + // Never let translation failures break the app + const finalFallback = fallback || path.split('.').pop() || 'Missing translation' + console.warn( + `Translation completely failed for "${path}", using fallback: "${finalFallback}"`, + ) + return finalFallback + } + } + + return { + tObject: (strings: TemplateStringsArray | string) => { + const key = typeof strings === 'string' ? strings : String.raw(strings) + try { + return carefulGetWrapper(key) as UIStrings + } catch (error) { + console.warn(`tObject failed for "${key}":`, error) + return {} as UIStrings + } + }, + t: (strings: TemplateStringsArray | string, ...values: Array) => { + const key = typeof strings === 'string' ? strings : String.raw(strings, ...values) + // Provide specific fallbacks for common 404 page keys + const commonFallbacks: Record = { + oops: 'Ooops!', + github_docs: 'GitHub Docs', + } + const fallback = commonFallbacks[key] || commonFallbacks[key.split('.').pop() || ''] + return carefulGetWrapper(key, fallback) as string + }, + } +} + +/** + * Server-side translation function for App Router pages + * Enhanced with better error handling for missing keys and defensive fallbacks + */ +export function translate(uiData: UIStrings, key: string, fallback?: string): string { + // Defensive check for completely missing data + if (!uiData || typeof uiData !== 'object') { + console.warn(`UI data is missing or corrupted for key "${key}", using fallback`) + return getCommonFallback(key, fallback) + } + + try { + return carefulGet(uiData, key) as string + } catch (error) { + const finalFallback = getCommonFallback(key, fallback) + + // Only warn in development + if (process.env.NODE_ENV === 'development') { + console.warn( + `Server translation failed for "${key}":`, + error instanceof Error ? error.message : error, + `Using fallback: "${finalFallback}"`, + ) + } + + return finalFallback + } +} + +/** + * Get common fallback values for essential UI keys + */ +function getCommonFallback(key: string, providedFallback?: string): string { + const commonFallbacks: Record = { + 'meta.oops': 'Ooops!', + 'header.github_docs': 'GitHub Docs', + 'meta.default_description': 'Get started, troubleshoot, and make the most of GitHub.', + 'footer.terms': 'Terms', + 'footer.privacy': 'Privacy', + 'footer.status': 'Status', + 'support.contact_support': 'Contact support', + } + + return ( + commonFallbacks[key] || + providedFallback || + commonFallbacks[key.split('.').pop() || ''] || + key.split('.').pop() || + key + ) +} + +function carefulGet(uiData: UIStrings, dottedPath: string) { + const splitPath = dottedPath.split('.') + const start = splitPath[0] + if (!(start in uiData)) { + throw new UngettableError( + `Namespace "${start}" not found in loaded data (available: ${Object.keys(uiData).sort()})`, + ) + } + if (splitPath.length > 1) { + const deeper = uiData[start] + if (typeof deeper === 'string') { + throw new Error(`Namespace "${start}" is a string, not an object`) + } + return carefulGet(deeper, splitPath.slice(1).join('.')) + } else { + if (!(start in uiData)) { + throw new UngettableError(`Key "${start}" not found in loaded data`) + } + return uiData[start] + } +} diff --git a/src/pages/404.tsx b/src/pages/404.tsx deleted file mode 100644 index 8fbbfedfdaaf..000000000000 --- a/src/pages/404.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { SimpleHeader, SimpleFooter } from '@/frame/components/GenericError' -import Head from 'next/head' -import { CommentDiscussionIcon } from '@primer/octicons-react' -import { Lead } from '@/frame/components/ui/Lead' - -const Custom404 = () => { - return ( -
- - 404 - Page not found - - - - - -
-
-

Ooops!

- It looks like this page doesn't exist. -

- We track these errors automatically, but if the problem persists please feel free to - contact us. -

- - - Contact support - -
-
- - -
- ) -} - -export default Custom404 diff --git a/src/pages/_notfound.tsx b/src/pages/_notfound.tsx deleted file mode 100644 index fbd4a82dc2af..000000000000 --- a/src/pages/_notfound.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import Custom404 from './404' - -export default function NotFound() { - return -} diff --git a/tsconfig.json b/tsconfig.json index 30546d01de4e..7bc98921f899 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2022", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -18,13 +22,25 @@ "allowSyntheticDefaultImports": true, "incremental": true, "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "plugins": [ + { + "name": "next" + } + ] }, "exclude": [ "node_modules", "docs-internal-data", "src/code-scanning/scripts/generate-code-scanning-query-list.ts" ], - "include": ["*.d.ts", "**/*.ts", "**/*.tsx"] + "include": [ + "**/*.ts", + "**/*.tsx", + "*.d.ts", + ".next/types/**/*.ts" + ] } From c2acbd0a2288d0e1de927876f08b2135f2bc59c2 Mon Sep 17 00:00:00 2001 From: Anne-Marie <102995847+am-stead@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:21:22 +0100 Subject: [PATCH 3/4] Dependencies (for Issues and Projects) #16501 (#56296) Co-authored-by: Kevin Heis --- .../best-practices-for-projects.md | 4 ++- .../about-issues.md | 4 +++ ...-tracking-work-for-your-team-or-project.md | 6 +++- .../creating-issue-dependencies.md | 36 +++++++++++++++++++ .../using-issues/index.md | 1 + .../issues/issue-dependencies-preview-note.md | 1 + 6 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 content/issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies.md create mode 100644 data/reusables/issues/issue-dependencies-preview-note.md diff --git a/content/issues/planning-and-tracking-with-projects/learning-about-projects/best-practices-for-projects.md b/content/issues/planning-and-tracking-with-projects/learning-about-projects/best-practices-for-projects.md index aff3290f2f3c..208625fd7f22 100644 --- a/content/issues/planning-and-tracking-with-projects/learning-about-projects/best-practices-for-projects.md +++ b/content/issues/planning-and-tracking-with-projects/learning-about-projects/best-practices-for-projects.md @@ -21,7 +21,9 @@ You can use {% data variables.product.prodname_projects_v2 %} to manage your wor Breaking a large issue into smaller issues makes the work more manageable and enables team members to work in parallel. It also leads to smaller pull requests, which are easier to review. -To track how smaller issues fit into the larger goal, milestones, or labels. For more information, see [AUTOTITLE](/issues/using-labels-and-milestones-to-track-work/about-milestones) and [AUTOTITLE](/issues/using-labels-and-milestones-to-track-work/managing-labels). +To ensure efficient progress, clearly define which issues are blocked by, or blocking, other issues. See [AUTOTITLE](/free-pro-team@latest/issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies). + +To track how smaller issues fit into the larger goal, use milestones or labels. For more information, see [AUTOTITLE](/issues/using-labels-and-milestones-to-track-work/about-milestones) and [AUTOTITLE](/issues/using-labels-and-milestones-to-track-work/managing-labels). ## Communicate diff --git a/content/issues/tracking-your-work-with-issues/about-issues.md b/content/issues/tracking-your-work-with-issues/about-issues.md index 8461a4aa91be..449f90732fe2 100644 --- a/content/issues/tracking-your-work-with-issues/about-issues.md +++ b/content/issues/tracking-your-work-with-issues/about-issues.md @@ -35,6 +35,10 @@ Issues can be created in a variety of ways, so you can choose the most convenien {% endif %} +## About issue dependencies + +You can define blocking relationships between issues using issue dependencies. Issue dependencies let you identify issues that are blocked by, or blocking, other work. See [AUTOTITLE](/free-pro-team@latest/issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies). + ## About integration with {% data variables.product.github %} Issues integrate with your work all across {% data variables.product.github %}. Mentioning an issue in another issue or pull request will create references between them and using keywords, like `fixes:`, in your pull requests will automatically close the associated issues. See [AUTOTITLE](/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue). diff --git a/content/issues/tracking-your-work-with-issues/configuring-issues/planning-and-tracking-work-for-your-team-or-project.md b/content/issues/tracking-your-work-with-issues/configuring-issues/planning-and-tracking-work-for-your-team-or-project.md index 75be679726f8..4bf98f7748ff 100644 --- a/content/issues/tracking-your-work-with-issues/configuring-issues/planning-and-tracking-work-for-your-team-or-project.md +++ b/content/issues/tracking-your-work-with-issues/configuring-issues/planning-and-tracking-work-for-your-team-or-project.md @@ -92,6 +92,10 @@ Below we have added a task list to our Project Octocat issue, breaking it down i {% endif %} +### Showing which issues are blocked by, or blocking, other work + +By creating issue dependencies, you can easily see and communicate which issues are blocked by, or blocking, other issues. This helps streamline coordination, prevent bottlenecks and increase transparency across the team. See [AUTOTITLE](/free-pro-team@latest/issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies). + {% ifversion copilot %} ## Understanding new issues @@ -175,7 +179,7 @@ You can also use the existing {% data variables.product.prodname_projects_v1 %} Below is a {% data variables.projects.projects_v1_board %} for our example Project Octocat with the issue we created, and the smaller issues we broke it down into, added to it. -![Screenshot of a {% data variables.projects.projects_v1_board %} called "Project Octocat Board," with issues organized into columns for "To do", "In progress," and "Done."](/assets/images/help/issues/quickstart-project-board.png) +![Screenshot of a {% data variables.projects.projects_v1_board %} called "Project Octocat Board," with issues organized into columns for "To do," "In progress," and "Done."](/assets/images/help/issues/quickstart-project-board.png) {% endif %} diff --git a/content/issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies.md b/content/issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies.md new file mode 100644 index 000000000000..da30e7809b8b --- /dev/null +++ b/content/issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies.md @@ -0,0 +1,36 @@ +--- +title: Creating issue dependencies +intro: 'Learn how to create issue dependencies so that you can see which issues are blocked by, or blocking, other work.' +versions: + fpt: '*' + ghec: '*' +type: overview +topics: + - Project management +permissions: 'People with at least triage permissions for a repository can create issue dependencies.' +product: 'Issue dependencies are available for users on {% data variables.product.prodname_free_user %}, {% data variables.product.prodname_pro %}, {% data variables.product.prodname_team %}, and {% data variables.product.prodname_ghe_cloud %} plans.' +--- + +{% data reusables.issues.issue-dependencies-preview-note %} + +Issue dependencies let you define issues that are blocked by, or blocking, other work. + +## Marking an issue as blocked by, or blocking, another issue + +1. Navigate to the issue that you want to create a dependency for. +1. In the right sidebar, click **Relationships**. +1. From the dropdown, select a dependency option: + * To indicate that your issue depends on another issue being completed, select **Mark as blocked by**. + * To indicate that your issue is preventing another issue from being completed, select **Mark as blocking**. +1. In the dialog box that opens, search for and select all the issues that are blocked by, or blocking, your issue. + +Blocked issues are marked with a "Blocked" icon on your project boards or repository's Issues page, so you can easily identify bottlenecks. + +## Removing a blocking relationship between two issues + +1. Navigate to the issue that you want to remove a dependency from. +1. In the right sidebar, click **Relationships**. +1. From the dropdown, select a dependency option: + * To indicate that your issue no longer depends on another issue being completed, select **Change blocked by**. + * To indicate that your issue is no longer preventing another issue from being completed, select **Change blocking**. +1. In the dialog box that opens, deselect the issues that are no longer blocked by, or blocking, your issue. diff --git a/content/issues/tracking-your-work-with-issues/using-issues/index.md b/content/issues/tracking-your-work-with-issues/using-issues/index.md index bfa3a7b76aaf..ec7f7593ac7a 100644 --- a/content/issues/tracking-your-work-with-issues/using-issues/index.md +++ b/content/issues/tracking-your-work-with-issues/using-issues/index.md @@ -10,6 +10,7 @@ topics: children: - /creating-an-issue - /adding-sub-issues + - /creating-issue-dependencies - /assigning-issues-and-pull-requests-to-other-github-users - /editing-an-issue - /viewing-all-of-your-issues-and-pull-requests diff --git a/data/reusables/issues/issue-dependencies-preview-note.md b/data/reusables/issues/issue-dependencies-preview-note.md new file mode 100644 index 000000000000..83eda79b55f4 --- /dev/null +++ b/data/reusables/issues/issue-dependencies-preview-note.md @@ -0,0 +1 @@ +> [!NOTE] Issue dependencies are in public preview and subject to change. From a3113544d316316b7d1290622067196d7c3088ff Mon Sep 17 00:00:00 2001 From: Anne-Marie <102995847+am-stead@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:50:31 +0100 Subject: [PATCH 4/4] Copilot Generates Commit Messages on .com #18776 (#57024) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sheena Ganju Co-authored-by: Sophie <29382425+sophietheking@users.noreply.github.com> --- .../copilot-commit-message-generation.md | 68 +++++++++++++++++++ content/copilot/responsible-use/index.md | 1 + 2 files changed, 69 insertions(+) create mode 100644 content/copilot/responsible-use/copilot-commit-message-generation.md diff --git a/content/copilot/responsible-use/copilot-commit-message-generation.md b/content/copilot/responsible-use/copilot-commit-message-generation.md new file mode 100644 index 000000000000..092049a3dc8f --- /dev/null +++ b/content/copilot/responsible-use/copilot-commit-message-generation.md @@ -0,0 +1,68 @@ +--- +title: Responsible use of GitHub Copilot commit message generation +shortTitle: Commit message generation +allowTitleToDifferFromFilename: true +intro: 'Learn how to use {% data variables.product.prodname_copilot_short %} commit message generation responsibly by understanding its purposes, capabilities, and limitations.' +versions: + feature: copilot +topics: + - Copilot +contentType: rai +--- + +## About {% data variables.product.prodname_copilot_short %} commit message generation + +{% data variables.product.prodname_copilot_short %} commit message generation is an AI-powered feature that allows you to create a commit message summary (title) and description based on the changes you've selected to commit in {% data variables.product.prodname_dotcom_the_website %}. To learn about commit message generation in {% data variables.product.prodname_desktop %}, see [AUTOTITLE](/copilot/responsible-use/copilot-in-github-desktop). + +When users commit changes to files using {% data variables.product.github %}'s web interface, {% data variables.product.prodname_copilot_short %} scans through the code changes and provides a suggested summary (title) and description of the changes made in prose. You can review and edit {% data variables.product.prodname_copilot_short %}'s suggested title and description **before** committing the changes to a branch. + +The only supported language for {% data variables.product.prodname_copilot_short %}-generated commit messages in {% data variables.product.prodname_dotcom_the_website %} is English. + +{% data variables.product.prodname_copilot_short %} commit message generation uses a simple-prompt flow leveraging the {% data variables.product.prodname_copilot_short %} API, utilizing the generic large language model and no additional trained models. + +When you click on the **Commit changes** button in {% data variables.product.prodname_dotcom_the_website %}, a call is generated to the {% data variables.product.prodname_copilot_short %} API to generate suggested text to insert into the summary and description boxes. The text complete request includes information from the selected changes in the different files of the repository in a prompt that requests {% data variables.product.prodname_copilot_short %} to generate a suggestion for a commit message that accurately describes those changes. The response is then used to fill the summary and description boxes. You can then review the suggested message, edit it if needed, and then make a commit with it. + +## Use cases for {% data variables.product.prodname_copilot_short %} commit message generation + +{% data variables.product.prodname_copilot_short %} commit message generation aims to streamline the author workflow so that they can save time and maintain clear commit histories when summarizing their changes. For many users, this could be helpful for saving time when committing large changes. Authors can review and edit suggestions before finalizing and manually committing the changes to a branch. The feature is integrated seamlessly into the commit workflow for a smoother experience. + +## Improving {% data variables.product.prodname_copilot_short %} commit message generation + +To enhance the experience and address some of the limitations of {% data variables.product.prodname_copilot_short %} commit message generation, there are various measures that you can adopt. For more information about the limitations, see [Limitations of {% data variables.product.prodname_copilot_short %} commit message generation](#limitations-of-copilot-commit-message-generation). + +### Use {% data variables.product.prodname_copilot_short %} commit message generation as a tool, not a replacement + +The feature is intended to supplement rather than replace a human's work to draft commit messages. The quality of the commit message suggestions will depend on the quality of the code changes and the context in the changed files. It remains your responsibility to review and assess the accuracy of information in the commits you create. + +### Provide feedback + +If you encounter any issues or limitations with {% data variables.product.prodname_copilot_short %} commit message generation, you can provide feedback via the [community discussion](https://github.com/orgs/community/discussions/categories/copilot-news-and-announcements). This can help the developers to improve the tool and address any concerns or limitations. + +## Limitations of {% data variables.product.prodname_copilot_short %} commit message generation + +Depending on factors such as your operating system and input data, you may encounter different levels of accuracy when using {% data variables.product.prodname_copilot_short %} commit message generation in {% data variables.product.prodname_dotcom_the_website %}. The following information is designed to help you understand system limitations and key concepts about performance as they apply to {% data variables.product.prodname_copilot_short %} commit message generation. + +### Limited scope + +{% data variables.product.prodname_copilot_short %} commit message generation operates within defined boundaries and might struggle with intricate code changes, short diff windows, or recently developed programming languages. The quality of suggestions it provides can be influenced by the availability and diversity of training data. For instance, inquiries about well-documented languages like Python may yield more accurate responses compared to questions about less popular languages. + +### Inaccurate responses + +The more inputs and context that {% data variables.product.prodname_copilot_short %} can learn from, the better the outputs will become. However, since the feature is quite new, it will take time to reach exact precision with the summaries that are generated. In the meantime, there may be cases where a generated summary is less accurate and requires the user to make modifications before saving and publishing their commit. In addition, there is a risk of "hallucination," where {% data variables.product.prodname_copilot_short %} generates statements that are inaccurate. For these reasons, reviewing is a requirement, and careful review of the output is highly recommended by our team. + +### Replication of commit message content + +Because a commit message is a summary of the changes that were made in a repository, there is potential for the summary to include harmful or offensive terms if any are within the content of the changes. + +### Potential biases and errors + +Training data for {% data variables.product.prodname_copilot_short %} commit message generation is sourced from existing online sources. It’s important to note that these sources may include biases and errors of the individuals who contributed to the training data. {% data variables.product.prodname_copilot_short %} commit message generation may inadvertently perpetuate these biases and errors. + +## Opt out + +Users wishing to opt out of {% data variables.product.prodname_copilot_short %} commit message generation can do so via the {% data variables.product.prodname_copilot_short %} [settings page](https://github.com/settings/copilot/features) in {% data variables.product.prodname_dotcom_the_website %}. + +## Further reading + +* [AUTOTITLE](/free-pro-team@latest/site-policy/github-terms/github-terms-for-additional-products-and-features#github-copilot) +* [{% data variables.product.prodname_copilot %} Trust Center](https://copilot.github.trust.page/) diff --git a/content/copilot/responsible-use/index.md b/content/copilot/responsible-use/index.md index 6cf7899a6f6c..a9ba0b455f41 100644 --- a/content/copilot/responsible-use/index.md +++ b/content/copilot/responsible-use/index.md @@ -16,6 +16,7 @@ children: - /copilot-in-github-desktop - /pull-request-summaries - /copilot-text-completion + - /copilot-commit-message-generation - /code-review - /copilot-coding-agent - /spark