diff --git a/content/README.md b/content/README.md index c0806e63ae49..e2740dfeef4a 100644 --- a/content/README.md +++ b/content/README.md @@ -259,7 +259,9 @@ includeGuides: - `id` (required): Unique identifier for the journey. The id only needs to be unique for journeys within a single journey landing page. - `title` (required): Display title for the journey (supports Liquid variables) - `description` (optional): Description of the journey (supports Liquid variables) - - `guides` (required): Array of article paths that make up this journey + - `guides` (required): Array of guide objects that make up this journey. Each guide object has: + - `href` (required): Path to the article + - `alternativeNextStep` (optional): Custom text to guide users to alternative paths in the journey. Supports Liquid variables and `[AUTOTITLE]`. - Only applicable when used with `layout: journey-landing`. - Optional. @@ -271,15 +273,16 @@ journeyTracks: title: 'Getting started with {% data variables.product.prodname_actions %}' description: 'Learn the basics of GitHub Actions.' guides: - - '/actions/quickstart' - - '/actions/learn-github-actions' - - '/actions/using-workflows' + - href: '/actions/quickstart' + - href: '/actions/learn-github-actions' + alternativeNextStep: 'Want to skip ahead? See [AUTOTITLE](/actions/using-workflows).' + - href: '/actions/using-workflows' - id: 'advanced' title: 'Advanced {% data variables.product.prodname_actions %}' description: 'Dive deeper into advanced features.' guides: - - '/actions/using-workflows/workflow-syntax-for-github-actions' - - '/actions/deployment/deploying-with-github-actions' + - href: '/actions/using-workflows/workflow-syntax-for-github-actions' + - href: '/actions/deployment/deploying-with-github-actions' ``` ### `type` diff --git a/content/contributing/writing-for-github-docs/using-yaml-frontmatter.md b/content/contributing/writing-for-github-docs/using-yaml-frontmatter.md index d66fafb3d19e..764cb95acb00 100644 --- a/content/contributing/writing-for-github-docs/using-yaml-frontmatter.md +++ b/content/contributing/writing-for-github-docs/using-yaml-frontmatter.md @@ -257,7 +257,9 @@ includeGuides: * `id` (required): Unique identifier for the journey. The id only needs to be unique for journeys within a single journey landing page. * `title` (required): Display title for the journey (supports Liquid variables) * `description` (optional): Description of the journey (supports Liquid variables) - * `guides` (required): Array of article paths that make up this journey + * `guides` (required): Array of guide objects that make up this journey. Each guide object has: + * `href` (required): Path to the article + * `alternativeNextStep` (optional): Custom text to guide users to alternative paths in the journey. Supports Liquid variables and `[AUTOTITLE]`. * Only applicable when used with `layout: journey-landing`. * Optional. @@ -269,15 +271,16 @@ journeyTracks: title: 'Getting started with {% data variables.product.prodname_actions %}' description: 'Learn the basics of GitHub Actions.' guides: - - '/actions/quickstart' - - '/actions/learn-github-actions' - - '/actions/using-workflows' + - href: '/actions/quickstart' + - href: '/actions/learn-github-actions' + alternativeNextStep: 'Want to skip ahead? See [AUTOTITLE](/actions/using-workflows).' + - href: '/actions/using-workflows' - id: 'advanced' title: 'Advanced {% data variables.product.prodname_actions %}' description: 'Dive deeper into advanced features.' guides: - - '/actions/using-workflows/workflow-syntax-for-github-actions' - - '/actions/deployment/deploying-with-github-actions' + - href: '/actions/using-workflows/workflow-syntax-for-github-actions' + - href: '/actions/deployment/deploying-with-github-actions' ``` ### `type` diff --git a/content/copilot/concepts/agents/code-review.md b/content/copilot/concepts/agents/code-review.md index 5d428c67f997..6c5461b52fae 100644 --- a/content/copilot/concepts/agents/code-review.md +++ b/content/copilot/concepts/agents/code-review.md @@ -37,10 +37,29 @@ This article provides an overview of {% data variables.copilot.copilot_code-revi * Xcode * JetBrains IDEs -{% data variables.copilot.copilot_code-review_short %} is a premium feature, available with the {% data variables.copilot.copilot_pro_short %}, {% data variables.copilot.copilot_pro_plus_short %}, {% data variables.copilot.copilot_business_short %}, and {% data variables.copilot.copilot_enterprise_short %} plans. See [Copilot plans](https://github.com/features/copilot/plans?ref_product=copilot&ref_type=purchase&ref_style=text). +{% data variables.copilot.copilot_code-review_short %} is a premium feature, available with the {% data variables.copilot.copilot_pro_short %}, {% data variables.copilot.copilot_pro_plus_short %}, {% data variables.copilot.copilot_business_short %}, and {% data variables.copilot.copilot_enterprise_short %} plans. See [{% data variables.product.prodname_copilot_short %} plans](https://github.com/features/copilot/plans?ref_product=copilot&ref_type=purchase&ref_style=text). If you receive {% data variables.product.prodname_copilot_short %} from an organization then, to be able to request a pull request review from {% data variables.product.prodname_copilot_short %} on {% data variables.product.prodname_dotcom_the_website %} or in {% data variables.product.prodname_mobile %}, the **{% data variables.copilot.copilot_code-review_short %}** option must be enabled in the {% data variables.product.prodname_copilot_short %} policy settings for the organization. See [AUTOTITLE](/copilot/how-tos/administer/organizations/managing-policies-for-copilot-in-your-organization). +## {% data variables.copilot.copilot_code-review_short %} without a {% data variables.product.prodname_copilot_short %} license + +{% data variables.copilot.copilot_code-review_short %} is also available on {% data variables.product.prodname_dotcom_the_website %} for organization members **without a {% data variables.product.prodname_copilot_short %} license**, when enabled by an enterprise administrator or organization owner. This capability is available to organizations on the **{% data variables.copilot.copilot_business_short %}** and **{% data variables.copilot.copilot_enterprise_short %}** plans. + +To allow organization members without a {% data variables.product.prodname_copilot_short %} license to use {% data variables.copilot.copilot_code-review_short %}, you must enable two policies: + +1. **Premium request paid usage**. This must be enabled first. It allows the enterprise or organization to incur charges for {% data variables.copilot.copilot_code-review_short %} premium request usage. +1. **Allow members without a {% data variables.product.prodname_copilot_short %} license to use {% data variables.copilot.copilot_code-review_short %} in {% data variables.product.prodname_dotcom_the_website %}**. This sub-policy enables {% data variables.copilot.copilot_code-review_short %} for users without a license. + + * This policy is **disabled by default**. + * Once this policy is set at the enterprise level, it becomes **visible (but not editable)** at the organization level. + * The policy is **most restrictive**, meaning {% data variables.copilot.copilot_code-review_short %} is only available in repositories where the policy is explicitly enabled. + +When both policies are enabled, users without a {% data variables.product.prodname_copilot_short %} license can request a {% data variables.copilot.copilot_code-review_short %} review on their pull requests in the organization’s repositories. + +In addition, in repositories where automatic code review is enabled, {% data variables.product.prodname_copilot_short %} automatically reviews all pull requests, regardless of whether the author has a {% data variables.product.prodname_copilot_short %} license. + +{% data variables.copilot.copilot_code-review_short %} for users without a license is **not available in IDEs** or in organizations that do not have these policies enabled. + ## Excluded files Dependency management files (such as package.json and Gemfile.lock) and certain other types of files (such as log files and SVGs) are excluded from {% data variables.copilot.copilot_code-review_short %}. If you include any of these files in a pull request, {% data variables.copilot.copilot_code-review_short %} will not consider the file when carrying out the review. Similarly, using {% data variables.copilot.copilot_code-review_short %} on one of these files in your IDE, will not generate review comments. @@ -52,10 +71,11 @@ For more information, see [AUTOTITLE](/copilot/reference/review-excluded-files). > [!NOTE] > > * The [AUTOTITLE](/free-pro-team@latest/site-policy/github-terms/github-pre-release-license-terms) apply to your use of this product. -> * These tools are enabled automatically for {% data variables.copilot.copilot_pro %} or {% data variables.copilot.copilot_pro_plus %} plans. -> * If you get a {% data variables.product.prodname_copilot_short %} subscription from an organization, you will only be able to participate in the {% data variables.release-phases.public_preview %} on the {% data variables.product.github %} website if an owner of your organization or enterprise has enabled **Copilot in GitHub.com > Opt in to preview features** in the **{% data variables.product.prodname_copilot %} policies** page of the organization or enterprise settings. See [AUTOTITLE](/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-policies-for-copilot-in-your-organization#enabling-copilot-features-in-your-organization) and [AUTOTITLE](/copilot/how-tos/administer-copilot/manage-for-enterprise/manage-enterprise-policies). +> * {% data variables.copilot.copilot_code-review_short %} has several new tools that are in {% data variables.release-phases.public_preview %} and subject to change. + +These new tools are enabled automatically for {% data variables.copilot.copilot_pro_short %} or {% data variables.copilot.copilot_pro_plus_short %} plans. -{% data variables.copilot.copilot_code-review_short %} has several new tools that are in {% data variables.release-phases.public_preview %} and subject to change. +If you get a {% data variables.product.prodname_copilot_short %} subscription from an organization, you will only be able to participate in the {% data variables.release-phases.public_preview %} on the {% data variables.product.github %} website if an owner of your organization or enterprise has enabled **{% data variables.product.prodname_copilot_short %} in {% data variables.product.prodname_dotcom_the_website %} > Opt in to preview features** in the **{% data variables.product.prodname_copilot %} policies** page of the organization or enterprise settings. See [AUTOTITLE](/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-policies-for-copilot-in-your-organization#enabling-copilot-features-in-your-organization) and [AUTOTITLE](/copilot/how-tos/administer-copilot/manage-for-enterprise/manage-enterprise-policies). * **Full project context gathering** to provide more specific, accurate, and contextually aware code reviews. * **Support for static analysis tools like {% data variables.product.prodname_codeql %}, ESLint, and PMD** to deliver more high-signal, consistent findings for security and quality. @@ -73,12 +93,18 @@ In the event that {% data variables.product.prodname_actions %} is unavailable o ## Code review monthly quota -Each time {% data variables.product.prodname_copilot_short %} reviews a pull request, or reviews code in your IDE, your monthly quota of Copilot premium requests is reduced by one. +Each time {% data variables.product.prodname_copilot_short %} reviews a pull request, or reviews code in your IDE, your monthly quota of {% data variables.product.prodname_copilot_short %} premium requests is reduced by one. If a repository is configured to automatically request a code review from {% data variables.product.prodname_copilot_short %} for all new pull requests, the premium request usage is applied to the quota of the pull request author. If a pull request is created by {% data variables.product.prodname_actions %} or by a bot, the usage will apply to the user who triggered the workflow (if identifiable), or to a designated billing owner. When you reach your monthly quota you will not be able to get a code review from {% data variables.product.prodname_copilot_short %} until your quota resets—unless you upgrade your {% data variables.product.prodname_copilot_short %} plan or enable additional premium requests. +### Quota for users without a {% data variables.product.prodname_copilot_short %} license + +Users without a {% data variables.product.prodname_copilot_short %} license do not have a monthly premium request quota. When {% data variables.copilot.copilot_code-review_short %} is enabled for these users through the “Allow members without a {% data variables.product.prodname_copilot_short %} license to use {% data variables.copilot.copilot_code-review_short %} in {% data variables.product.prodname_dotcom_the_website %}” policy, any premium requests they generate—either by manually requesting a review or through automatic code review—are billed directly to the organization or enterprise as paid overage usage. + +Premium requests generated by users without a license are not attributed to any {% data variables.product.prodname_copilot_short %} plan quota and appear as overage usage in billing reports and premium request analytics. Users with a {% data variables.product.prodname_copilot_short %} license continue to consume premium requests from their assigned plan quota. + ## Model usage {% data reusables.copilot.ccr-model-usage %} diff --git a/content/copilot/how-tos/manage-and-track-spending/monitor-premium-requests.md b/content/copilot/how-tos/manage-and-track-spending/monitor-premium-requests.md index e7612a22e6e3..54e6b0f931c8 100644 --- a/content/copilot/how-tos/manage-and-track-spending/monitor-premium-requests.md +++ b/content/copilot/how-tos/manage-and-track-spending/monitor-premium-requests.md @@ -58,11 +58,11 @@ You can view an overview of your premium request usage at any time in your "Bill ### Viewing detailed analytics of your usage +{% data reusables.billing.premium-request-analytics-start %} + {% data reusables.user-settings.access_billing_settings_url %} 1. In the side bar, click **Premium request analytics** to show detailed analytics. 1. Use the filter, "Group by", and "Timeframe" options to change the data displayed in the chart and table. - {% data reusables.billing.premium-request-analytics-start %} - 1. Optionally, to download the data shown in the chart, click the {% octicon "kebab-horizontal" aria-label="Chart options" aria-hidden="true" %} button and select your preferred format. ![Screenshot of the usage chart on the "Premium request analytics" page with "Chart options" open and outlined in dark orange.](/assets/images/help/billing/premium-request-analytics-chart-download.png) diff --git a/content/copilot/how-tos/use-copilot-agents/request-a-code-review/use-code-review.md b/content/copilot/how-tos/use-copilot-agents/request-a-code-review/use-code-review.md index 0d2df4d7931d..5098fdc65474 100644 --- a/content/copilot/how-tos/use-copilot-agents/request-a-code-review/use-code-review.md +++ b/content/copilot/how-tos/use-copilot-agents/request-a-code-review/use-code-review.md @@ -28,6 +28,8 @@ For a full introduction to {% data variables.copilot.copilot_code-review %}, see {% webui %} +{% data variables.copilot.copilot_code-review_short %} is also available for organization members without a {% data variables.product.prodname_copilot_short %} license, when enabled by an enterprise administrator or organization owner. See [{% data variables.copilot.copilot_code-review_short %} for organization members without a {% data variables.product.prodname_copilot_short %} license](/copilot/concepts/agents/code-review#copilot-code-review-for-organization-members-without-a-copilot-license). + ## Using {% data variables.copilot.copilot_code-review_short %} These instructions explain how to use {% data variables.copilot.copilot_code-review_short %} in the {% data variables.product.github %} website. To see instructions for other popular coding environments, click the appropriate tab at the top of the page. diff --git a/content/copilot/reference/ai-models/model-comparison.md b/content/copilot/reference/ai-models/model-comparison.md index 0add3515cbc1..03100c0a1677 100644 --- a/content/copilot/reference/ai-models/model-comparison.md +++ b/content/copilot/reference/ai-models/model-comparison.md @@ -42,6 +42,7 @@ Use this table to find a suitable model quickly, see more detail in the sections | {% data variables.copilot.copilot_claude_opus_41 %} | Deep reasoning and debugging | Complex problem-solving challenges, sophisticated reasoning | Reasoning, vision | [{% data variables.copilot.copilot_claude_opus_41 %} model card](https://assets.anthropic.com/m/4c024b86c698d3d4/original/Claude-4-1-System-Card.pdf) | | {% data variables.copilot.copilot_claude_sonnet_40 %} | Deep reasoning and debugging | Performance and practicality, perfectly balanced for coding workflows | Agent mode, vision | [{% data variables.copilot.copilot_claude_sonnet_40 %} model card](https://www-cdn.anthropic.com/6be99a52cb68eb70eb9572b4cafad13df32ed995.pdf) | | {% data variables.copilot.copilot_gemini_25_pro %} | Deep reasoning and debugging | Complex code generation, debugging, and research workflows | Reasoning, vision | [{% data variables.copilot.copilot_gemini_25_pro %} model card](https://storage.googleapis.com/model-cards/documents/gemini-2.5-pro.pdf) | +| {% data variables.copilot.copilot_gemini_3_flash %} | Fast help with simple or repetitive tasks | Fast, reliable answers to lightweight coding questions | Agent mode | Not available | | {% data variables.copilot.copilot_grok_code %} | General-purpose coding and writing | Fast, accurate code completions and explanations | Agent mode | [{% data variables.copilot.copilot_grok_code %} model card](https://data.x.ai/2025-08-20-grok-4-model-card.pdf) | | {% data variables.copilot.copilot_qwen_25 %} | General-purpose coding and writing | Code generation, reasoning, and code repair / debugging | Reasoning | [{% data variables.copilot.copilot_qwen_25 %} model card](https://arxiv.org/pdf/2409.12186) | | {% data variables.copilot.copilot_raptor_mini %} | General-purpose coding and writing | Fast, accurate code completions and explanations | Agent mode | Coming soon | diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md index 34174dd09d9f..b01c17e5b77b 100644 --- a/content/copilot/reference/ai-models/model-hosting.md +++ b/content/copilot/reference/ai-models/model-hosting.md @@ -72,8 +72,9 @@ Used for: * {% data variables.copilot.copilot_gemini_25_pro %} * {% data variables.copilot.copilot_gemini_3_pro %} +* {% data variables.copilot.copilot_gemini_3_flash %} -{% data variables.product.prodname_copilot %} uses {% data variables.copilot.copilot_gemini_3_pro %} and {% data variables.copilot.copilot_gemini_25_pro %} hosted on Google Cloud Platform (GCP). When using {% data variables.copilot.copilot_gemini %} models, prompts and metadata are sent to GCP, which makes the [following data commitment](https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance): _{% data variables.copilot.copilot_gemini %} doesn't use your prompts, or its responses, as data to train its models._ +{% data variables.product.prodname_copilot %} uses {% data variables.copilot.copilot_gemini_3_pro %}, {% data variables.copilot.copilot_gemini_3_flash %}, and {% data variables.copilot.copilot_gemini_25_pro %} hosted on Google Cloud Platform (GCP). When using {% data variables.copilot.copilot_gemini %} models, prompts and metadata are sent to GCP, which makes the [following data commitment](https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance): _{% data variables.copilot.copilot_gemini %} doesn't use your prompts, or its responses, as data to train its models._ To provide better service quality and reduce latency, {% data variables.product.github %} uses [prompt caching](https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance#customer_data_retention_and_achieving_zero_data_retention). diff --git a/content/copilot/reference/policy-conflicts.md b/content/copilot/reference/policy-conflicts.md index da063328f39a..7c7b406af5aa 100644 --- a/content/copilot/reference/policy-conflicts.md +++ b/content/copilot/reference/policy-conflicts.md @@ -42,6 +42,7 @@ Feature, model, and privacy settings for users are set according to the **least | :---- | :---- | :---- | | {% data variables.product.prodname_copilot_short %} Metrics API | Most restrictive organization | [AUTOTITLE](/rest/copilot/copilot-metrics) | | Suggestions matching public code (privacy policy) | Most restrictive organization | [AUTOTITLE](/copilot/concepts/completions/code-suggestions) | +| Allow members without a {% data variables.product.prodname_copilot_short %} license to use {% data variables.copilot.copilot_code-review_short %} in {% data variables.product.prodname_dotcom_the_website %} | Most restrictive organization | [AUTOTITLE](/copilot/responsible-use/code-review) | | {% data variables.product.prodname_copilot_short %} can search the web | Least restrictive organization | [AUTOTITLE](/copilot/responsible-use/chat-in-github#leveraging-a-web-search-to-answer-a-question) | | {% data variables.copilot.copilot_mobile_short %} | Least restrictive organization | [AUTOTITLE](/copilot/responsible-use/chat-in-github-mobile) | | {% data variables.copilot.copilot_chat_short %} in the IDE | Least restrictive organization | [AUTOTITLE](/copilot/responsible-use/chat-in-your-ide) | diff --git a/content/enterprise-onboarding/index.md b/content/enterprise-onboarding/index.md index a3dffca05b40..c79a08f5d179 100644 --- a/content/enterprise-onboarding/index.md +++ b/content/enterprise-onboarding/index.md @@ -16,56 +16,56 @@ journeyTracks: title: 'Getting started with your enterprise' description: 'Master the fundamentals of {% data variables.product.prodname_ghe_cloud %} and get started with a trial.' guides: - - '/enterprise-onboarding/getting-started-with-your-enterprise/choose-an-enterprise-type' - - '/enterprise-onboarding/getting-started-with-your-enterprise/setting-up-a-trial-of-github-enterprise' - - '/enterprise-onboarding/getting-started-with-your-enterprise/adding-users-to-your-enterprise' - - '/enterprise-onboarding/getting-started-with-your-enterprise/about-enterprise-billing' - - '/enterprise-onboarding/getting-started-with-your-enterprise/about-migrating-to-github-enterprise-cloud' + - href: '/enterprise-onboarding/getting-started-with-your-enterprise/choose-an-enterprise-type' + - href: '/enterprise-onboarding/getting-started-with-your-enterprise/setting-up-a-trial-of-github-enterprise' + - href: '/enterprise-onboarding/getting-started-with-your-enterprise/adding-users-to-your-enterprise' + - href: '/enterprise-onboarding/getting-started-with-your-enterprise/about-enterprise-billing' + - href: '/enterprise-onboarding/getting-started-with-your-enterprise/about-migrating-to-github-enterprise-cloud' - id: 'setting_up_organizations_and_teams' title: 'Setting up organizations and teams in your enterprise' description: 'Organize work effectively and ensure people have the access they need to resources and administrative settings.' guides: - - '/enterprise-onboarding/setting-up-organizations-and-teams/best-practices' - - '/enterprise-onboarding/setting-up-organizations-and-teams/setting-up-an-organization' - - '/enterprise-onboarding/setting-up-organizations-and-teams/about-roles-in-an-enterprise' - - '/enterprise-onboarding/setting-up-organizations-and-teams/identify-role-requirements' - - '/enterprise-onboarding/setting-up-organizations-and-teams/creating-custom-roles' - - '/enterprise-onboarding/setting-up-organizations-and-teams/about-teams-in-an-enterprise' - - '/enterprise-onboarding/setting-up-organizations-and-teams/creating-teams' - - '/enterprise-onboarding/setting-up-organizations-and-teams/assigning-roles-to-teams-and-users' - - '/enterprise-onboarding/setting-up-organizations-and-teams/use-innersource' + - href: '/enterprise-onboarding/setting-up-organizations-and-teams/best-practices' + - href: '/enterprise-onboarding/setting-up-organizations-and-teams/setting-up-an-organization' + - href: '/enterprise-onboarding/setting-up-organizations-and-teams/about-roles-in-an-enterprise' + - href: '/enterprise-onboarding/setting-up-organizations-and-teams/identify-role-requirements' + - href: '/enterprise-onboarding/setting-up-organizations-and-teams/creating-custom-roles' + - href: '/enterprise-onboarding/setting-up-organizations-and-teams/about-teams-in-an-enterprise' + - href: '/enterprise-onboarding/setting-up-organizations-and-teams/creating-teams' + - href: '/enterprise-onboarding/setting-up-organizations-and-teams/assigning-roles-to-teams-and-users' + - href: '/enterprise-onboarding/setting-up-organizations-and-teams/use-innersource' - id: 'support_for_your_enterprise' title: 'Creating a support model for your enterprise' description: 'Find out how to get help and choose who will be able to contact Support.' guides: - - '/enterprise-onboarding/support-for-your-enterprise/understanding-support' - - '/enterprise-onboarding/support-for-your-enterprise/using-the-support-portal' - - '/enterprise-onboarding/support-for-your-enterprise/managing-support-entitlements' + - href: '/enterprise-onboarding/support-for-your-enterprise/understanding-support' + - href: '/enterprise-onboarding/support-for-your-enterprise/using-the-support-portal' + - href: '/enterprise-onboarding/support-for-your-enterprise/managing-support-entitlements' - id: 'govern_people_and_repositories' title: 'Governing people and repositories' description: 'Implement policies, custom properties, and rulesets to govern users and repositories across your enterprise.' guides: - - '/enterprise-onboarding/govern-people-and-repositories/about-enterprise-policies' - - '/enterprise-onboarding/govern-people-and-repositories/create-custom-properties' - - '/enterprise-onboarding/govern-people-and-repositories/create-repository-policies' - - '/enterprise-onboarding/govern-people-and-repositories/protect-branches' - - '/enterprise-onboarding/govern-people-and-repositories/using-the-audit-log-for-your-enterprise' - - '/enterprise-onboarding/govern-people-and-repositories/about-enterprise-security' + - href: '/enterprise-onboarding/govern-people-and-repositories/about-enterprise-policies' + - href: '/enterprise-onboarding/govern-people-and-repositories/create-custom-properties' + - href: '/enterprise-onboarding/govern-people-and-repositories/create-repository-policies' + - href: '/enterprise-onboarding/govern-people-and-repositories/protect-branches' + - href: '/enterprise-onboarding/govern-people-and-repositories/using-the-audit-log-for-your-enterprise' + - href: '/enterprise-onboarding/govern-people-and-repositories/about-enterprise-security' - id: 'github_apps' title: 'Automating processes with GitHub Apps' description: 'Create and install apps to automate processes securely in your enterprise and organizations.' guides: - - '/enterprise-onboarding/github-apps/create-enterprise-apps' - - '/enterprise-onboarding/github-apps/install-enterprise-apps' + - href: '/enterprise-onboarding/github-apps/create-enterprise-apps' + - href: '/enterprise-onboarding/github-apps/install-enterprise-apps' - id: 'github_actions_for_your_enterprise' title: 'Setting up CI/CD with GitHub Actions' description: 'Explore {% data variables.product.prodname_actions %}, plan your rollout, and get started.' guides: - - '/enterprise-onboarding/github-actions-for-your-enterprise/about-github-actions-for-enterprises' - - '/enterprise-onboarding/github-actions-for-your-enterprise/actions-components' - - '/enterprise-onboarding/github-actions-for-your-enterprise/planning-a-rollout-of-github-actions' - - '/enterprise-onboarding/github-actions-for-your-enterprise/migrating-your-enterprise-to-github-actions' - - '/enterprise-onboarding/github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-cloud' + - href: '/enterprise-onboarding/github-actions-for-your-enterprise/about-github-actions-for-enterprises' + - href: '/enterprise-onboarding/github-actions-for-your-enterprise/actions-components' + - href: '/enterprise-onboarding/github-actions-for-your-enterprise/planning-a-rollout-of-github-actions' + - href: '/enterprise-onboarding/github-actions-for-your-enterprise/migrating-your-enterprise-to-github-actions' + - href: '/enterprise-onboarding/github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-cloud' versions: ghec: '*' topics: diff --git a/content/get-started/start-your-journey/index.md b/content/get-started/start-your-journey/index.md index ec7ad779f112..8fad16bcef0f 100644 --- a/content/get-started/start-your-journey/index.md +++ b/content/get-started/start-your-journey/index.md @@ -28,12 +28,12 @@ journeyTracks: title: 'Get started' description: 'Master the fundamentals of {% data variables.product.github %} and Git.' guides: - - '/get-started/start-your-journey/about-github-and-git' - - '/get-started/start-your-journey/creating-an-account-on-github' - - '/get-started/start-your-journey/hello-world' - - '/get-started/start-your-journey/setting-up-your-profile' - - '/get-started/start-your-journey/finding-inspiration-on-github' - - '/get-started/start-your-journey/downloading-files-from-github' - - '/get-started/start-your-journey/uploading-a-project-to-github' - - '/get-started/start-your-journey/git-and-github-learning-resources' + - href: '/get-started/start-your-journey/about-github-and-git' + - href: '/get-started/start-your-journey/creating-an-account-on-github' + - href: '/get-started/start-your-journey/hello-world' + - href: '/get-started/start-your-journey/setting-up-your-profile' + - href: '/get-started/start-your-journey/finding-inspiration-on-github' + - href: '/get-started/start-your-journey/downloading-files-from-github' + - href: '/get-started/start-your-journey/uploading-a-project-to-github' + - href: '/get-started/start-your-journey/git-and-github-learning-resources' --- diff --git a/data/reusables/billing/premium-request-analytics-start.md b/data/reusables/billing/premium-request-analytics-start.md index 94ebd042fbf4..c052161099ae 100644 --- a/data/reusables/billing/premium-request-analytics-start.md +++ b/data/reusables/billing/premium-request-analytics-start.md @@ -1,2 +1,8 @@ > [!NOTE] > Premium request analytics data are available from **August 1, 2025** onward. Separate usage data for features that use premium requests is available from **November 1, 2025** for {% data variables.product.prodname_copilot_short %}, {% data variables.product.prodname_spark_short %}, and {% data variables.copilot.copilot_coding_agent %}. + + +User-level analytics have different access permissions depending on your role. + +* Enterprise owners and billing managers can see premium request analytics by user. +* Organization owners cannot view premium request analytics by user or use the `user` parameter in the API. To view user-level usage, these users can download a premium request usage report. See [AUTOTITLE](/billing/how-tos/products/view-productlicense-use#downloading-usage-reports). diff --git a/data/tables/copilot/model-multipliers.yml b/data/tables/copilot/model-multipliers.yml index 65d5e2a47a6b..9c00a3aa4af7 100644 --- a/data/tables/copilot/model-multipliers.yml +++ b/data/tables/copilot/model-multipliers.yml @@ -33,6 +33,10 @@ multiplier_paid: 1 multiplier_free: Not applicable +- name: Gemini 3 Flash + multiplier_paid: 0.33 + multiplier_free: Not applicable + - name: Gemini 3 Pro multiplier_paid: 1 multiplier_free: Not applicable diff --git a/data/tables/copilot/model-release-status.yml b/data/tables/copilot/model-release-status.yml index c945e4c38ebc..e4936a7ad41b 100644 --- a/data/tables/copilot/model-release-status.yml +++ b/data/tables/copilot/model-release-status.yml @@ -126,6 +126,13 @@ ask_mode: true edit_mode: true +- name: 'Gemini 3 Flash' + provider: 'Google' + release_status: 'Public preview' + agent_mode: true + ask_mode: true + edit_mode: true + - name: 'Gemini 3 Pro' provider: 'Google' release_status: 'Public preview' diff --git a/data/tables/copilot/model-supported-clients.yml b/data/tables/copilot/model-supported-clients.yml index 19be4ce35424..e05ac3db7801 100644 --- a/data/tables/copilot/model-supported-clients.yml +++ b/data/tables/copilot/model-supported-clients.yml @@ -61,6 +61,14 @@ xcode: true jetbrains: true +- name: Gemini 3 Flash + dotcom: true + vscode: true + vs: false + eclipse: false + xcode: false + jetbrains: false + - name: Gemini 3 Pro dotcom: true vscode: true diff --git a/data/tables/copilot/model-supported-plans.yml b/data/tables/copilot/model-supported-plans.yml index 4ca0d4ebe2d4..b6f84a50c743 100644 --- a/data/tables/copilot/model-supported-plans.yml +++ b/data/tables/copilot/model-supported-plans.yml @@ -54,6 +54,13 @@ business: true enterprise: true +- name: Gemini 3 Flash + free: false + pro: true + pro_plus: true + business: true + enterprise: true + - name: Gemini 3 Pro free: false pro: true diff --git a/data/variables/copilot.yml b/data/variables/copilot.yml index 4501e7bcc063..bca40b07c0a6 100644 --- a/data/variables/copilot.yml +++ b/data/variables/copilot.yml @@ -147,6 +147,7 @@ copilot_claude_sonnet_45: 'Claude Sonnet 4.5' # Gemini: copilot_gemini: 'Gemini' copilot_gemini_flash: 'Gemini 2.0 Flash' +copilot_gemini_3_flash: 'Gemini 3 Flash' copilot_gemini_25_pro: 'Gemini 2.5 Pro' copilot_gemini_3_pro: 'Gemini 3 Pro' # OpenAI GPT series: diff --git a/src/content-linter/lib/linting-rules/journey-tracks-guide-path-exists.ts b/src/content-linter/lib/linting-rules/journey-tracks-guide-path-exists.ts index 17bfbe04c3af..5ded02cac234 100644 --- a/src/content-linter/lib/linting-rules/journey-tracks-guide-path-exists.ts +++ b/src/content-linter/lib/linting-rules/journey-tracks-guide-path-exists.ts @@ -71,14 +71,19 @@ export const journeyTracksGuidePathExists = { const trackObj = track as Record if (trackObj.guides && Array.isArray(trackObj.guides)) { for (let guideIndex = 0; guideIndex < trackObj.guides.length; guideIndex++) { - const guide: string = trackObj.guides[guideIndex] - if (typeof guide === 'string') { - if (!isValidGuidePath(guide, params.name)) { + const guideObj = trackObj.guides[guideIndex] + + // Validate guide is an object with expected properties + if (!guideObj || typeof guideObj !== 'object') continue + + // Validate href property + if ('href' in guideObj && typeof guideObj.href === 'string') { + if (!isValidGuidePath(guideObj.href, params.name)) { addError( onError, journeyTracksLineNumber, - `Journey track guide path does not exist: ${guide} (track ${trackIndex + 1}, guide ${guideIndex + 1})`, - guide, + `Journey track guide path does not exist: ${guideObj.href} (track ${trackIndex + 1}, guide ${guideIndex + 1})`, + guideObj.href, ) } } diff --git a/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts b/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts index b125c043c1fd..bb602fc1c14d 100644 --- a/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts +++ b/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts @@ -75,16 +75,38 @@ export const journeyTracksLiquid = { if (track.guides && Array.isArray(track.guides)) { for (let guideIndex = 0; guideIndex < track.guides.length; guideIndex++) { - const guide: string = track.guides[guideIndex] - if (typeof guide === 'string') { + const guideObj = track.guides[guideIndex] + + // Validate guide is an object with expected properties + if (!guideObj || typeof guideObj !== 'object') continue + + // Validate href property + if ('href' in guideObj && typeof guideObj.href === 'string') { + try { + liquid.parse(guideObj.href) + } catch (error: any) { + addError( + onError, + trackLineNumber, + `Invalid Liquid syntax in journey track guide href (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error.message}`, + guideObj.href, + ) + } + } + + // Validate alternativeNextStep property if present + if ( + 'alternativeNextStep' in guideObj && + typeof guideObj.alternativeNextStep === 'string' + ) { try { - liquid.parse(guide) + liquid.parse(guideObj.alternativeNextStep) } catch (error: any) { addError( onError, trackLineNumber, - `Invalid Liquid syntax in journey track guide (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error.message}`, - guide, + `Invalid Liquid syntax in journey track guide alternativeNextStep (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error.message}`, + guideObj.alternativeNextStep, ) } } diff --git a/src/content-linter/tests/fixtures/journey-tracks/duplicate-ids.md b/src/content-linter/tests/fixtures/journey-tracks/duplicate-ids.md index bcc808a81367..a54559bc8dc8 100644 --- a/src/content-linter/tests/fixtures/journey-tracks/duplicate-ids.md +++ b/src/content-linter/tests/fixtures/journey-tracks/duplicate-ids.md @@ -11,15 +11,15 @@ journeyTracks: - id: duplicate-id title: "First Track" guides: - - /article-one + - href: /article-one - id: unique-id title: "Unique Track" guides: - - /article-two + - href: /article-two - id: duplicate-id title: "Second Track with Same ID" guides: - - /subdir/article-three + - href: /subdir/article-three --- # Journey with Duplicate IDs diff --git a/src/content-linter/tests/fixtures/journey-tracks/invalid-paths.md b/src/content-linter/tests/fixtures/journey-tracks/invalid-paths.md index ef4f7d558bc1..c863f9c0f0df 100644 --- a/src/content-linter/tests/fixtures/journey-tracks/invalid-paths.md +++ b/src/content-linter/tests/fixtures/journey-tracks/invalid-paths.md @@ -11,9 +11,9 @@ journeyTracks: - id: track-1 title: "Track with Invalid Guides" guides: - - /article-one - - /nonexistent/guide - - /another/invalid/path + - href: /article-one + - href: /nonexistent/guide + - href: /another/invalid/path --- # Journey with Invalid Paths diff --git a/src/content-linter/tests/fixtures/journey-tracks/non-journey-layout.md b/src/content-linter/tests/fixtures/journey-tracks/non-journey-layout.md index a7dcec80ebc9..53b5599487f2 100644 --- a/src/content-linter/tests/fixtures/journey-tracks/non-journey-layout.md +++ b/src/content-linter/tests/fixtures/journey-tracks/non-journey-layout.md @@ -11,7 +11,7 @@ journeyTracks: - id: track-1 title: "Should be ignored" guides: - - /nonexistent/path + - href: /nonexistent/path --- # Non-Journey Page diff --git a/src/content-linter/tests/fixtures/journey-tracks/valid-journey.md b/src/content-linter/tests/fixtures/journey-tracks/valid-journey.md index 30b98033b436..bf64df138981 100644 --- a/src/content-linter/tests/fixtures/journey-tracks/valid-journey.md +++ b/src/content-linter/tests/fixtures/journey-tracks/valid-journey.md @@ -11,12 +11,12 @@ journeyTracks: - id: track-1 title: "Getting Started Track" guides: - - /article-one - - /article-two + - href: /article-one + - href: /article-two - id: track-2 title: "Advanced Track" guides: - - /subdir/article-three + - href: /subdir/article-three --- # Valid Journey Landing diff --git a/src/content-linter/tests/unit/journey-tracks.ts b/src/content-linter/tests/unit/journey-tracks.ts index f097eb3f16ae..08615adadbfe 100644 --- a/src/content-linter/tests/unit/journey-tracks.ts +++ b/src/content-linter/tests/unit/journey-tracks.ts @@ -40,7 +40,7 @@ journeyTracks: title: "Track with {% invalid liquid" description: "Description with {{ unclosed liquid" guides: - - /article-one + - href: /article-one --- # Journey with Liquid Issues @@ -55,6 +55,36 @@ This journey landing page has invalid liquid syntax in journeyTracks. expect(result['test-invalid-liquid.md'][0].ruleDescription).toMatch(/liquid syntax/i) expect(result['test-invalid-liquid.md'][1].ruleDescription).toMatch(/liquid syntax/i) }) + + test('invalid liquid syntax in alternativeNextStep fails', async () => { + const invalidAlternativeNextStepContent = `--- +title: Journey with Invalid Alternative Next Step +layout: journey-landing +versions: + fpt: '*' + ghec: '*' + ghes: '*' +journeyTracks: + - id: track-1 + title: "Test Track" + guides: + - href: /article-one + alternativeNextStep: "Want to skip? See {% invalid liquid syntax" +--- + +# Journey with Invalid Alternative Next Step +` + const result = await runRule(journeyTracksLiquid, { + strings: { 'test-invalid-alternative-next-step.md': invalidAlternativeNextStepContent }, + ...fmOptions, + }) + + expect(result['test-invalid-alternative-next-step.md']).toHaveLength(1) + expect(result['test-invalid-alternative-next-step.md'][0].errorDetail).toMatch( + /alternativeNextStep/, + ) + expect(result['test-invalid-alternative-next-step.md'][0].errorDetail).toMatch(/liquid syntax/i) + }) }) describe('journey-tracks-guide-path-exists', () => { diff --git a/src/fixtures/fixtures/content/get-started/foo/index.md b/src/fixtures/fixtures/content/get-started/foo/index.md index e49f67728324..774984fc4731 100644 --- a/src/fixtures/fixtures/content/get-started/foo/index.md +++ b/src/fixtures/fixtures/content/get-started/foo/index.md @@ -18,4 +18,5 @@ children: - /page-with-permissions-and-product-callout - /table-with-ifversions - /code-snippet-with-hashbang + - /journey-test-article --- diff --git a/src/fixtures/fixtures/content/get-started/foo/journey-test-article.md b/src/fixtures/fixtures/content/get-started/foo/journey-test-article.md new file mode 100644 index 000000000000..e6541e22874c --- /dev/null +++ b/src/fixtures/fixtures/content/get-started/foo/journey-test-article.md @@ -0,0 +1,12 @@ +--- +title: Journey Test Article +intro: This article is used for testing journey branching text. +versions: + fpt: '*' + ghes: '*' + ghec: '*' +--- + +## Test Article + +This article exists solely for testing journey navigation features. diff --git a/src/fixtures/fixtures/content/get-started/index.md b/src/fixtures/fixtures/content/get-started/index.md index f49326331f7e..45693f0c7003 100644 --- a/src/fixtures/fixtures/content/get-started/index.md +++ b/src/fixtures/fixtures/content/get-started/index.md @@ -21,14 +21,14 @@ journeyTracks: title: 'Getting started' description: 'Learn the basics of our platform.' guides: - - '/get-started/start-your-journey/hello-world' - - '/get-started/foo/bar' + - href: '/get-started/start-your-journey/hello-world' + - href: '/get-started/foo/bar' - id: 'advanced' title: 'Advanced topics' description: 'Dive deeper into advanced features.' guides: - - '/get-started/foo/autotitling' - - '/get-started/start-your-journey/hello-world' + - href: '/get-started/foo/autotitling' + - href: '/get-started/start-your-journey/hello-world' children: - /start-your-journey - /foo diff --git a/src/fixtures/fixtures/content/get-started/test-journey/index.md b/src/fixtures/fixtures/content/get-started/test-journey/index.md index 1b30f95696ac..c6cc9365b545 100644 --- a/src/fixtures/fixtures/content/get-started/test-journey/index.md +++ b/src/fixtures/fixtures/content/get-started/test-journey/index.md @@ -7,18 +7,19 @@ versions: ghes: '*' ghec: '*' journeyTracks: - - id: 'getting_started' - title: 'Getting started' - description: 'Learn the basics of our platform.' + - id: 'first_track' + title: 'First Track' + description: 'The first track in the journey.' guides: - - '/get-started/start-your-journey/hello-world' - - '/get-started/foo/bar' - - id: 'advanced' - title: 'Advanced topics' - description: 'Dive deeper into advanced features.' + - href: '/get-started/start-your-journey/hello-world' + - href: '/get-started/foo/journey-test-article' + alternativeNextStep: 'Want to skip ahead? See [AUTOTITLE](/get-started/start-your-journey/hello-world)' + - id: 'second_track' + title: 'Next Track' + description: 'The second track in the journey.' guides: - - '/get-started/foo/autotitling' - - '/get-started/start-your-journey/hello-world' + - href: '/get-started/foo/autotitling' + - href: '/get-started/start-your-journey/hello-world' --- This is a test page for journey tracks. diff --git a/src/fixtures/tests/playwright-rendering.spec.ts b/src/fixtures/tests/playwright-rendering.spec.ts index fd7d8b895e92..385439074e44 100644 --- a/src/fixtures/tests/playwright-rendering.spec.ts +++ b/src/fixtures/tests/playwright-rendering.spec.ts @@ -1139,6 +1139,53 @@ test.describe('Journey Tracks', () => { expect(trackContent).not.toContain('{%') expect(trackContent).not.toContain('%}') }) + + test('journey navigation components show on article pages', async ({ page }) => { + // go to an article that's part of a journey track + await page.goto('/get-started/start-your-journey/hello-world') + + // journey card should be visible in sidebar + const journeyCard = page.locator('[data-testid="journey-track-card"]') + await expect(journeyCard).toBeVisible() + + // journey footer nav should be visible + const journeyNav = page.locator('[data-testid="journey-track-nav"]') + await expect(journeyNav).toBeVisible() + }) + + test('journey footer nav component links to first article in next track from last article in previous track', async ({ + page, + }) => { + await page.goto('/get-started/foo/bar') + + const journeyNav = page.locator('[data-testid="journey-track-nav"]') + await expect(journeyNav).toBeVisible() + + // Link should display the next track's title and go to its first article + const nextTrackLink = journeyNav.locator('a').filter({ hasText: 'Advanced topics' }) + await expect(nextTrackLink).toBeVisible() + + const href = await nextTrackLink.getAttribute('href') + expect(href).toContain('/get-started/foo/autotitling') + }) + + test('journey card displays branching text when present', async ({ page }) => { + await page.goto('/get-started/foo/journey-test-article') + + const journeyCard = page.locator('[data-testid="journey-track-card"]') + await expect(journeyCard).toBeVisible() + + // Branching text should be rendered with markdown links + await expect(journeyCard).toContainText('Want to skip ahead?') + + // AUTOTITLE should be resolved to actual article title + const branchingLink = journeyCard.locator('a').filter({ hasText: 'Hello World' }) + await expect(branchingLink).toBeVisible() + await expect(journeyCard).not.toContainText('AUTOTITLE') + + const href = await branchingLink.getAttribute('href') + expect(href).toContain('/get-started/start-your-journey/hello-world') + }) }) test.describe('LandingArticleGridWithFilter component', () => { diff --git a/src/frame/components/article/ArticlePage.tsx b/src/frame/components/article/ArticlePage.tsx index 89ef49ed8842..629655fc9f5e 100644 --- a/src/frame/components/article/ArticlePage.tsx +++ b/src/frame/components/article/ArticlePage.tsx @@ -22,6 +22,7 @@ import { Link } from '@/frame/components/Link' import { useTranslation } from '@/languages/components/useTranslation' import { LinkPreviewPopover } from '@/links/components/LinkPreviewPopover' import { UtmPreserver } from '@/frame/components/UtmPreserver' +import { JourneyTrackCard, JourneyTrackNav } from '@/journeys/components' const ClientSideRefresh = dynamic(() => import('@/frame/components/ClientSideRefresh'), { ssr: false, @@ -42,10 +43,12 @@ export const ArticlePage = () => { productVideoUrl, miniTocItems, currentLearningTrack, + currentJourneyTrack, supportPortalVaIframeProps, currentLayout, } = useArticleContext() const isLearningPath = !!currentLearningTrack?.trackName + const isJourneyTrack = !!currentJourneyTrack?.trackId const { t } = useTranslation(['pages']) const introProp = ( @@ -72,6 +75,7 @@ export const ArticlePage = () => { const toc = ( <> {isLearningPath && } + {isJourneyTrack && } {miniTocItems.length > 1 && } ) @@ -122,6 +126,11 @@ export const ArticlePage = () => { ) : null} + {isJourneyTrack ? ( +
+ +
+ ) : null} ) : (
@@ -148,6 +157,11 @@ export const ArticlePage = () => {
) : null} + {isJourneyTrack ? ( +
+ +
+ ) : null} )} diff --git a/src/frame/lib/frontmatter.ts b/src/frame/lib/frontmatter.ts index 3beca783efd6..a923d1b06bc5 100644 --- a/src/frame/lib/frontmatter.ts +++ b/src/frame/lib/frontmatter.ts @@ -247,7 +247,20 @@ export const schema: Schema = { guides: { type: 'array', items: { - type: 'string', + type: 'object', + required: ['href'], + properties: { + href: { + type: 'string', + description: 'Path to the article in the journey track', + }, + alternativeNextStep: { + type: 'string', + description: + 'Optional branching text for the article when guiding users through the journey', + }, + }, + additionalProperties: false, }, description: 'Array of article paths that make up this journey track', }, diff --git a/src/ghes-releases/lib/enterprise-dates.json b/src/ghes-releases/lib/enterprise-dates.json index 61f6659d96f7..08f687be2dd9 100644 --- a/src/ghes-releases/lib/enterprise-dates.json +++ b/src/ghes-releases/lib/enterprise-dates.json @@ -258,9 +258,9 @@ "generalAvailabilityDate": "2025-10-14" }, "3.19": { - "releaseDate": "2025-11-11", + "releaseDate": "2025-12-02", "deprecationDate": "2026-12-09", - "releaseCandidateDate": "2025-11-11", + "releaseCandidateDate": "2025-12-02", "generalAvailabilityDate": "2025-12-09" }, "3.20": { diff --git a/src/journeys/components/JourneyTrackCard.tsx b/src/journeys/components/JourneyTrackCard.tsx index 26a84fcf4029..7eda7b69e682 100644 --- a/src/journeys/components/JourneyTrackCard.tsx +++ b/src/journeys/components/JourneyTrackCard.tsx @@ -13,9 +13,8 @@ export function JourneyTrackCard({ journey }: Props) { const { locale } = useRouter() const { currentVersion } = useVersion() const { t } = useTranslation('journey_track_nav') - const { trackTitle, journeyTitle, journeyPath, nextGuide, numberOfGuides, currentGuideIndex } = - journey - const fullPath = `/${locale}/${currentVersion}${journeyPath}?feature=journey-landing` + const { trackTitle, journeyPath, nextGuide, numberOfGuides, currentGuideIndex } = journey + const fullPath = `/${locale}/${currentVersion}${journeyPath}` return (

- {journeyTitle} + {trackTitle}

- {trackTitle} {t('current_progress') .replace('{n}', `${numberOfGuides}`) @@ -49,6 +47,12 @@ export function JourneyTrackCard({ journey }: Props) { )} + {journey.alternativeNextStep && ( +
+ )}
) diff --git a/src/journeys/components/JourneyTrackNav.tsx b/src/journeys/components/JourneyTrackNav.tsx index 91f71bc38368..4dba06ba3eae 100644 --- a/src/journeys/components/JourneyTrackNav.tsx +++ b/src/journeys/components/JourneyTrackNav.tsx @@ -8,7 +8,7 @@ type Props = { export function JourneyTrackNav({ context }: Props) { const { t } = useTranslation('journey_track_nav') - const { prevGuide, nextGuide, trackTitle, currentGuideIndex, numberOfGuides } = context + const { prevGuide, nextGuide, nextTrackFirstGuide } = context return (
- - {trackTitle} - - {t('current_progress') - .replace('{n}', `${numberOfGuides}`) - .replace('{i}', `${currentGuideIndex + 1}`)} - - - - {nextGuide && ( + {nextGuide ? ( <> {t('next_article')} {nextGuide.title} - )} + ) : nextTrackFirstGuide ? ( + <> + {t('next_article')} + + {nextTrackFirstGuide.trackTitle} + + + ) : null}
) diff --git a/src/journeys/lib/journey-path-resolver.ts b/src/journeys/lib/journey-path-resolver.ts index 1bc9b22088bc..b1136fbdece0 100644 --- a/src/journeys/lib/journey-path-resolver.ts +++ b/src/journeys/lib/journey-path-resolver.ts @@ -13,6 +13,11 @@ export interface JourneyContext { journeyPath: string currentGuideIndex: number numberOfGuides: number + nextTrackFirstGuide?: { + href: string + title: string + trackTitle: string + } nextGuide?: { href: string title: string @@ -21,6 +26,7 @@ export interface JourneyContext { href: string title: string } + alternativeNextStep?: string } export interface JourneyTrack { @@ -43,7 +49,10 @@ type JourneyPage = { id: string title: string description?: string - guides: string[] + guides: Array<{ + href: string + alternativeNextStep?: string + }> }> } @@ -83,6 +92,32 @@ function normalizeGuidePath(path: string): string { : `/${withoutLanguage || path}` } +/** + * Helper function to fetch guide data (href and title) for a given path + */ +async function fetchGuideData( + guidePath: string, + context: ContentContext, +): Promise<{ href: string; title: string } | null> { + try { + const resultData = await getLinkData(guidePath, context, { + title: true, + intro: false, + fullTitle: false, + }) + if (resultData && resultData.length > 0) { + const linkResult = resultData[0] + return { + href: linkResult.href, + title: linkResult.title || '', + } + } + } catch (error) { + console.warn('Could not get link data for guide:', guidePath, error) + } + return null +} + /** * Resolves the journey context for a given article path. * @@ -117,6 +152,8 @@ export async function resolveJourneyContext( } } + let trackIndex = 0 + let foundTrackIndex = 0 for (const track of journeyPage.journeyTracks) { if (!track.guides || !Array.isArray(track.guides)) continue @@ -124,7 +161,7 @@ export async function resolveJourneyContext( let guideIndex = -1 for (let i = 0; i < track.guides.length; i++) { - const guidePath = track.guides[i] + const guidePath = track.guides[i].href let renderedGuidePath = guidePath // Handle Liquid conditionals in guide paths @@ -148,6 +185,21 @@ export async function resolveJourneyContext( } if (guideIndex >= 0) { + const alternativeNextStep = track.guides[guideIndex].alternativeNextStep || '' + let renderedAlternativeNextStep = alternativeNextStep + + // Handle Liquid conditionals in branching text which likely has links + try { + renderedAlternativeNextStep = await executeWithFallback( + context, + () => renderContent(alternativeNextStep, context), + () => alternativeNextStep, + ) + } catch { + // If rendering fails, use the original branching text rather than erroring + renderedAlternativeNextStep = alternativeNextStep + } + result = { trackId: track.id, trackName: track.id, @@ -157,52 +209,51 @@ export async function resolveJourneyContext( journeyPage.permalink || Permalink.relativePathToSuffix(journeyPage.relativePath || ''), currentGuideIndex: guideIndex, numberOfGuides: track.guides.length, + alternativeNextStep: renderedAlternativeNextStep, } // Set up previous guide if (guideIndex > 0) { - const prevGuidePath = track.guides[guideIndex - 1] - try { - const resultData = await getLinkData(prevGuidePath, context, { - title: true, - intro: false, - fullTitle: false, - }) - if (resultData && resultData.length > 0) { - const linkResult = resultData[0] - result.prevGuide = { - href: linkResult.href, - title: linkResult.title || '', - } - } - } catch (error) { - console.warn('Could not get link data for previous guide:', prevGuidePath, error) + const prevGuidePath = track.guides[guideIndex - 1].href + const guideData = await fetchGuideData(prevGuidePath, context) + if (guideData) { + result.prevGuide = guideData } } // Set up next guide if (guideIndex < track.guides.length - 1) { - const nextGuidePath = track.guides[guideIndex + 1] - try { - const resultData = await getLinkData(nextGuidePath, context, { - title: true, - intro: false, - fullTitle: false, - }) - if (resultData && resultData.length > 0) { - const linkResult = resultData[0] - result.nextGuide = { - href: linkResult.href, - title: linkResult.title || '', + const nextGuidePath = track.guides[guideIndex + 1].href + const guideData = await fetchGuideData(nextGuidePath, context) + if (guideData) { + result.nextGuide = guideData + } + } + + // Only populate nextTrackFirstGuide when on the last guide of the track + if (guideIndex === track.guides.length - 1) { + foundTrackIndex = trackIndex + + if ( + journeyPage.journeyTracks[foundTrackIndex + 1] && + journeyPage.journeyTracks[foundTrackIndex + 1].guides.length > 0 + ) { + const nextTrack = journeyPage.journeyTracks[foundTrackIndex + 1] + const nextTrackFirstGuidePath = nextTrack.guides[0].href + const guideData = await fetchGuideData(nextTrackFirstGuidePath, context) + if (guideData) { + result.nextTrackFirstGuide = { + ...guideData, + trackTitle: nextTrack.title, } } - } catch (error) { - console.warn('Could not get link data for next guide:', nextGuidePath, error) } } break // Found the track, stop searching } + + trackIndex++ } if (result) break // Found the journey, stop searching @@ -217,11 +268,15 @@ export async function resolveJourneyContext( * Returns an array of JourneyTrack objects with titles, descriptions, and guide links. */ export async function resolveJourneyTracks( - journeyTracks: any[], + journeyTracks: JourneyPage['journeyTracks'], context: ContentContext, ): Promise { + if (!journeyTracks || journeyTracks.length === 0) { + return [] + } + const result = await Promise.all( - journeyTracks.map(async (track: any) => { + journeyTracks.map(async (track) => { // Render Liquid templates in title and description const renderedTitle = await renderContent(track.title, context, { textOnly: true }) const renderedDescription = track.description @@ -229,9 +284,9 @@ export async function resolveJourneyTracks( : undefined const guides = await Promise.all( - track.guides.map(async (guidePath: string) => { - const linkData = await getLinkData(guidePath, context, { title: true }) - const baseHref = linkData?.[0]?.href || guidePath + track.guides.map(async (guide: { href: string; alternativeNextStep?: string }) => { + const linkData = await getLinkData(guide.href, context, { title: true }) + const baseHref = linkData?.[0]?.href || guide.href return { href: baseHref, title: linkData?.[0]?.title || 'Untitled Guide', diff --git a/src/journeys/middleware/journey-track.ts b/src/journeys/middleware/journey-track.ts index e6e412785796..481ba58ca64b 100644 --- a/src/journeys/middleware/journey-track.ts +++ b/src/journeys/middleware/journey-track.ts @@ -9,29 +9,21 @@ export default async function journeyTrack( if (!req.context) throw new Error('request is not contextualized') if (!req.context.page) return next() - // Only run journey resolution if the page has journey tracks defined - if (!(req.context.page as any).journeyTracks) { - req.context.currentJourneyTrack = null - return next() - } - try { - // Import and use the journey resolver which uses renderContent, need the - // async import since it uses fs Node apis const journeyResolver = await import('../lib/journey-path-resolver') - // resolve the journey tracks which renders the journey content like the - // description to handle liquid rendering - const resolvedTracks = await journeyResolver.resolveJourneyTracks( - (req.context.page as any).journeyTracks, - req.context, - ) + // If this page has journey tracks defined, resolve them for the landing page + if ((req.context.page as any).journeyTracks) { + const resolvedTracks = await journeyResolver.resolveJourneyTracks( + (req.context.page as any).journeyTracks, + req.context, + ) - // Store resolved tracks on the page context for later use in getServerSideProps - ;(req.context.page as any).resolvedJourneyTracks = resolvedTracks + // Store resolved tracks on the page context for later use in getServerSideProps + ;(req.context.page as any).resolvedJourneyTracks = resolvedTracks + } - // resolve the current journey context since we're on a journey track page - // i.e. next/prev articles in the track, this article's position in the track + // Always try to resolve journey context (for navigation on guide articles) const journeyContext = await journeyResolver.resolveJourneyContext( req.pagePath || '', req.context.pages || {}, diff --git a/src/journeys/tests/journey-path-resolver.ts b/src/journeys/tests/journey-path-resolver.ts index 510b14a99a8b..891bd940cd16 100644 --- a/src/journeys/tests/journey-path-resolver.ts +++ b/src/journeys/tests/journey-path-resolver.ts @@ -46,11 +46,21 @@ describe('journey-path-resolver', () => { title: 'Getting started', description: 'Learn the basics', guides: [ - '/enterprise-onboarding/setup', - '/enterprise-onboarding/config', - '/enterprise-onboarding/deploy', + { href: '/enterprise-onboarding/setup' }, + { + href: '/enterprise-onboarding/config', + alternativeNextStep: + 'Ready for more? Visit [AUTOTITLE](/enterprise-onboarding/advanced-setup)', + }, + { href: '/enterprise-onboarding/deploy' }, ], }, + { + id: 'advanced', + title: 'Advanced configuration', + description: 'Configure advanced options', + guides: [{ href: '/enterprise-onboarding/advanced-setup' }], + }, ], }, } @@ -100,6 +110,28 @@ describe('journey-path-resolver', () => { }) }) + test('includes alternative next step when provided', async () => { + const result = await resolveJourneyContext( + '/enterprise-onboarding/config', + mockPages, + mockContext, + ) + + expect(result?.alternativeNextStep).toBe( + 'Ready for more? Visit [AUTOTITLE](/enterprise-onboarding/advanced-setup)', + ) + }) + + test('does not populate next track guide when not on last guide', async () => { + const result = await resolveJourneyContext( + '/enterprise-onboarding/config', + mockPages, + mockContext, + ) + + expect(result?.nextTrackFirstGuide).toBeUndefined() + }) + test('handles first article in track (no previous)', async () => { const result = await resolveJourneyContext( '/enterprise-onboarding/setup', @@ -122,6 +154,20 @@ describe('journey-path-resolver', () => { expect(result?.currentGuideIndex).toBe(2) }) + test('populates next track guide when on last guide', async () => { + const result = await resolveJourneyContext( + '/enterprise-onboarding/deploy', + mockPages, + mockContext, + ) + + expect(result?.nextTrackFirstGuide).toEqual({ + href: '/en/enterprise-cloud@latest/enterprise-onboarding/advanced-setup', + title: 'Mock Title for /enterprise-onboarding/advanced-setup', + trackTitle: 'Advanced configuration', + }) + }) + test('normalizes article paths without leading slash', async () => { // The resolver should handle paths without leading slashes // by normalizing them to match the guide paths in the data @@ -149,13 +195,16 @@ describe('journey-path-resolver', () => { id: 'getting_started', title: 'Getting started with {% data variables.product.company_short %}', description: 'Learn the {% data variables.product.company_short %} basics', - guides: ['/enterprise-onboarding/setup', '/enterprise-onboarding/config'], + guides: [ + { href: '/enterprise-onboarding/setup' }, + { href: '/enterprise-onboarding/config' }, + ], }, { id: 'advanced', title: 'Advanced configuration', description: 'Advanced topics for experts', - guides: ['/enterprise-onboarding/advanced-setup'], + guides: [{ href: '/enterprise-onboarding/advanced-setup' }], }, ] @@ -210,7 +259,7 @@ describe('journey-path-resolver', () => { { id: 'no_desc', title: 'Track without description', - guides: ['/some-guide'], + guides: [{ href: '/some-guide' }], }, ]