diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 645d00c4bae1..f337044a2876 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,7 +58,6 @@ jobs: - graphql - landings - languages - - learning-track # - links - observability # - open-source diff --git a/assets/images/help/actions/actions-oidc-settings.png b/assets/images/help/actions/actions-oidc-settings.png deleted file mode 100644 index 5bfd4d4a94a1..000000000000 Binary files a/assets/images/help/actions/actions-oidc-settings.png and /dev/null differ diff --git a/assets/images/help/dependabot/learners-enable-dependabot.png b/assets/images/help/dependabot/learners-enable-dependabot.png deleted file mode 100644 index 112fa28e7585..000000000000 Binary files a/assets/images/help/dependabot/learners-enable-dependabot.png and /dev/null differ diff --git a/assets/images/help/security-overview/security-coverage-view-summary-pre-config.png b/assets/images/help/security-overview/security-coverage-view-summary-pre-config.png deleted file mode 100644 index 5d4079b49f93..000000000000 Binary files a/assets/images/help/security-overview/security-coverage-view-summary-pre-config.png and /dev/null differ diff --git a/content/README.md b/content/README.md index 4c2a3dc2aa1b..5d8a363af188 100644 --- a/content/README.md +++ b/content/README.md @@ -23,9 +23,9 @@ See the [contributing docs](https://docs.github.com/en/contributing) for general - [`changelog`](#changelog) - [`defaultPlatform`](#defaultplatform) - [`defaultTool`](#defaulttool) - - [`learningTracks`](#learningtracks) - - [`includeGuides`](#includeguides) + - [`journeyTracks`](#journeytracks) + - [`journeyArticlesHeading`](#journeyarticlesheading) - [`contentType`](#contenttype) - [`communityRedirect`](#communityRedirect) - [`effectiveDate`](#effectiveDate) @@ -40,7 +40,6 @@ See the [contributing docs](https://docs.github.com/en/contributing) for general - [Legacy filepaths and redirects for links](#legacy-filepaths-and-redirects-for-links) - [Index pages](#index-pages) - [Home page](#homepage) - - [Creating new product guides pages](#creating-new-product-guides-pages) ## Frontmatter @@ -230,27 +229,6 @@ defaultPlatform: linux defaultTool: cli ``` -### `learningTracks` -- Purpose: Render a list of learning tracks on a product's sub-landing page. -- type: `String`. This should reference learning tracks' names defined in [`data/learning-tracks/*.yml`](../data/learning-tracks/README.md). -- Optional - -**Note: the featured track is set by a specific property in the learning tracks YAML. See that [README](../data/learning-tracks/README.md) for details.* - -### `includeGuides` -- Purpose: Render a list of articles, filterable by `contentType`. Only applicable when used with `layout: product-guides`. -- Type: `Array` -- Optional. - -Example: - -```yaml -includeGuides: - - /actions/guides/about-continuous-integration - - /actions/guides/setting-up-continuous-integration-using-workflow-templates - - /actions/guides/building-and-testing-nodejs - - /actions/guides/building-and-testing-powershell -``` ### `journeyTracks` - Purpose: Define journeys for journey landing pages. @@ -284,6 +262,24 @@ journeyTracks: - href: '/actions/deployment/deploying-with-github-actions' ``` +### `journeyArticlesHeading` +- Purpose: Override the default "Articles" heading shown above the article list on single-track journey landing pages. +- Type: `String` +- Only applicable when used with `layout: journey-landing` and a single journey track. +- Optional. If omitted, the heading defaults to the translated value of `journey_landing.articles_heading` ("Articles"). + +Example: + +```yaml +layout: journey-landing +journeyArticlesHeading: "Guides" +journeyTracks: + - id: ado_migration + title: Run your migration + guides: + - href: /migrations/ado/understand-migrations-from-azure-devops-to-github +``` + ### `contentType` - Purpose: Indicate the type of article. - Type: `String`, one of `get-started`, `concepts`, `how-tos`, `reference`, `tutorials`, `rai`, `landing` (only applies to `content//index.md` files). @@ -433,13 +429,3 @@ The homepage is the main Table of Contents file for the docs site. The homepage `childGroups` is an array of mappings containing a `name` for the group, an optional `icon` for the group, and an array of `children`. The `children` in the array must be present in the `children` frontmatter property. -### Creating new product guides pages - -To create a product guides page (e.g. [Actions' Guide page](https://docs.github.com/en/actions/guides)), create or modify an existing markdown file with these specific frontmatter values: - -1. Use the product guides page template by referencing `layout: product-guides`. -1. (optional) Include the learning tracks in [`learningTracks`](#learningTracks). -1. (optional) Define which articles to include with [`includeGuides`](#includeGuides). - -If using learning tracks, they need to be defined in [`data/learning-tracks/*.yml`](../data/learning-tracks/README.md). -If using `includeGuides`, make sure each of the articles in this list has [`contentType`](#contenttype) in its frontmatter. diff --git a/content/billing/how-tos/set-up-payment/connect-azure-sub.md b/content/billing/how-tos/set-up-payment/connect-azure-sub.md index 9bee484be5fd..9f4a282ddb02 100644 --- a/content/billing/how-tos/set-up-payment/connect-azure-sub.md +++ b/content/billing/how-tos/set-up-payment/connect-azure-sub.md @@ -8,6 +8,7 @@ redirect_from: - /billing/managing-billing-for-your-github-account/connecting-an-azure-subscription-to-your-enterprise - /billing/managing-billing-for-your-github-account/connecting-an-azure-subscription - /billing/managing-the-plan-for-your-github-account/connecting-an-azure-subscription + - /video-transcripts/transcript-billing-github-consumption-through-an-azure-subscription versions: fpt: '*' ghec: '*' diff --git a/content/code-security/concepts/secret-security/about-push-protection.md b/content/code-security/concepts/secret-security/about-push-protection.md index 7134a346b52b..c98d4e02f38c 100644 --- a/content/code-security/concepts/secret-security/about-push-protection.md +++ b/content/code-security/concepts/secret-security/about-push-protection.md @@ -18,7 +18,7 @@ category: ## What is push protection? -Push protection is a {% data variables.product.prodname_secret_scanning %} feature designed to prevent sensitive information, such as secrets or tokens, from ever being pushed to your repository. Unlike {% data variables.product.prodname_secret_scanning %}, which detects secrets after they have been committed, push protection proactively scans your code for secrets during the push process, then blocks the push if any are detected. +Push protection is a {% data variables.product.prodname_secret_scanning %} feature designed to prevent hardcoded credentials, such as secrets or tokens, from ever being pushed to your repository. Rather than alerting you to credential leaks after the fact, push protection blocks pushes that contain secrets _before_ they reach your repository. ## How push protection works @@ -74,9 +74,9 @@ If you want greater control over which contributors can bypass push protection a ## Benefits of push protection -* **Preventative security:** Push protection acts as a frontline defense mechanism by scanning code for secrets at the time of the push. This preventative approach helps to catch potential issues before they are merged into a repository. +* **Preventative security:** Push protection acts as a frontline defense mechanism by scanning code for hardcoded secrets at the time of the push. This preventative approach helps prevent credential leaks before they become ingrained in the repository's history, making it easier to address and remediate threats. * **Immediate feedback:** Developers receive instant feedback if a potential secret is detected during a push attempt. This immediate notification allows for quick remediation, reducing the likelihood of sensitive information being exposed. -* **Reduced risk of data leaks:** By blocking commits that contain sensitive information, push protection significantly reduces the risk of accidental data leaks. This helps in safeguarding against unauthorized access to your infrastructure, services, and data. +* **Reduced risk of credential leaks:** By blocking commits that contain hardcoded credentials, push protection significantly reduces the risk of accidental credential leaks and secret sprawl. This helps in safeguarding against potential breaches and maintaining the integrity of the codebase. * **Efficient secret management:** Instead of retrospectively dealing with exposed secrets, developers can address issues at the source. This makes secret management more efficient and less time-consuming. * **Bypass functionality for flexibility:** For cases where false positives occur or when certain patterns are necessary, you can bypass push protection for users, and designated users can use the delegated bypass feature to bypass push protection for repositories. {% ifversion push-protection-org-enterprise-exemptions %}Additionally, you can exempt trusted actors {% ifversion push-protection-repo-exemptions %}{% else %}at the organization and enterprise levels {% endif %}from push protection entirely. {% endif %}This provides flexibility without compromising overall security. * **Ability to detect custom patterns (for repositories in organizations):** Organizations can define custom patterns for detecting secrets unique to their environment. This customization ensures that push protection can effectively identify and block even non-standard secrets. diff --git a/content/code-security/concepts/secret-security/about-secret-scanning.md b/content/code-security/concepts/secret-security/about-secret-scanning.md index 52a5b9e74df8..06364fa70eb0 100644 --- a/content/code-security/concepts/secret-security/about-secret-scanning.md +++ b/content/code-security/concepts/secret-security/about-secret-scanning.md @@ -19,7 +19,7 @@ category: - Protect your secrets --- -When credentials like API keys and passwords are committed to repositories, they become targets for unauthorized access. {% data variables.product.prodname_secret_scanning_caps %} automatically detects these exposed secrets so you can secure them before they're exploited. +When credentials like API keys and passwords are committed to repositories as hardcoded secrets, they become targets for unauthorized access. {% data variables.product.prodname_secret_scanning_caps %} automatically detects credential leaks so you can secure them before they're exploited. {% ifversion secret-risk-assessment %} @@ -32,7 +32,7 @@ When credentials like API keys and passwords are committed to repositories, they ## How secret scanning protects your code -{% data variables.product.prodname_secret_scanning_caps %} scans your entire Git history on all branches of your repository for API keys, passwords, tokens, and other known secret types. {% data variables.product.github %} also periodically rescans repositories when new secret types are added. +{% data variables.product.prodname_secret_scanning_caps %} scans your entire Git history on all branches of your repository for hardcoded credentials, including API keys, passwords, tokens, and other known secret types. This helps you identify secret sprawl, the uncontrolled proliferation of credentials across repositories, before it becomes a security risk. {% data variables.product.github %} also periodically rescans repositories when new secret types are added. {% data variables.product.github %} also automatically scans: @@ -40,7 +40,7 @@ When credentials like API keys and passwords are committed to repositories, they ### {% data variables.product.prodname_secret_scanning_caps %} alerts and remediation -When {% data variables.product.prodname_secret_scanning %} finds a potential secret, {% data variables.product.github %} generates an alert on your repository's **{% data variables.product.prodname_security_and_quality_tab %}** tab with details about the exposed credential. +When {% data variables.product.prodname_secret_scanning %} detects a credential leak, {% data variables.product.github %} generates an alert on your repository's **{% data variables.product.prodname_security_and_quality_tab %}** tab with details about the exposed credential. When you receive an alert, rotate the affected credential immediately to prevent unauthorized access. While you can also remove secrets from your Git history, this is time-intensive and often unnecessary if you've already revoked the credential. diff --git a/content/code-security/concepts/secret-security/about-secret-security-with-github.md b/content/code-security/concepts/secret-security/about-secret-security-with-github.md index e54c8d8666e2..1f2de595eb1c 100644 --- a/content/code-security/concepts/secret-security/about-secret-security-with-github.md +++ b/content/code-security/concepts/secret-security/about-secret-security-with-github.md @@ -14,7 +14,7 @@ category: - Protect your secrets --- -Exposed secrets in your repositories can lead to unauthorized access, data breaches, and significant costs to your organization. For details about these risks and how to protect against them, see [AUTOTITLE](/code-security/concepts/secret-security/secret-leakage-risks). +Hardcoded credentials in your repositories can lead to credential leaks, unauthorized access, data breaches, and significant costs to your organization. For details about these risks and how to protect against them, see [AUTOTITLE](/code-security/concepts/secret-security/secret-leakage-risks). {% data variables.product.github %} provides tools to help you understand and address your organization's exposure to leaked secrets: @@ -23,7 +23,7 @@ Exposed secrets in your repositories can lead to unauthorized access, data breac ## Secret risk assessment -The secret risk assessment provides organization owners and security managers with a free point-in-time scan of their organization's repositories to identify leaked secrets like API keys, tokens, and passwords. +The secret risk assessment provides organization owners and security managers with a free point-in-time scan of their organization's repositories to identify hardcoded credentials like API keys, tokens, and passwords, and understand the extent of secret sprawl across your organization. {% data variables.secret-scanning.secret-risk-assessment-cta-product %} @@ -52,13 +52,13 @@ Regular assessment helps prevent: While the {% data variables.product.prodname_secret_risk_assessment %} provides a point-in-time view of your organization's current secret exposure, {% data variables.product.prodname_GH_secret_protection %}: * **Implements continuous monitoring** and expands scanned surfaces beyond code to include pull requests, issues, wikis, and discussions -* **Prevents secret leaks** by blocking commits containing secrets before they are saved to {% data variables.product.github %} +* **Prevents credential leaks** by blocking commits containing hardcoded secrets before they are saved to {% data variables.product.github %} * **Creates actionable alerts** that can be grouped into campaigns and assigned to team members for remediation * **Meets your specific needs** by scanning for patterns unique to your organization and unstructured secrets like passwords * **Supports governance at scale** with settings dictating who can bypass protections and dismiss alerts * **Surfaces key analytics** through a view dedicated to your organization's secret security -Through these features, {% data variables.product.prodname_GH_secret_protection %} provides complete coverage for your organization, reducing the risk of costly secret leaks and high-effort remediation processes. +Through these features, {% data variables.product.prodname_GH_secret_protection %} provides complete coverage for your organization, reducing the risk of costly credential leaks, secret sprawl, and high-effort remediation. For more information about the specific features of {% data variables.product.prodname_GH_secret_protection %}, see [AUTOTITLE](/code-security/getting-started/github-security-features#available-with-github-secret-protection). diff --git a/content/code-security/concepts/secret-security/secret-leakage-risks.md b/content/code-security/concepts/secret-security/secret-leakage-risks.md index 281e9faf710d..270b70c58e17 100644 --- a/content/code-security/concepts/secret-security/secret-leakage-risks.md +++ b/content/code-security/concepts/secret-security/secret-leakage-risks.md @@ -9,96 +9,90 @@ versions: contentType: concepts --- -## Secrets and credentials +## What are secrets? -Secrets are credentials that grant access to sensitive systems and data, including API keys, passwords, authentication tokens, certificates, and encryption keys. When secrets are committed to repositories, they become part of your Git history and remain accessible even after being removed from the latest commit. +Secrets are credentials that grant access to sensitive systems and data. Common examples include: -Secrets in code repositories can be discovered by automated scanning tools and unauthorized users. Public repositories are particularly vulnerable, but leaked credentials from private repositories can also spread through forks, CI/CD logs, or third-party integrations. +* API keys and tokens used to authenticate with external services +* Database passwords and connection strings +* Cloud provider credentials and service account tokens +* Certificates and encryption keys -## Security impact of exposed secrets +When secrets are committed to repositories, they become **hardcoded credentials** that are embedded directly in your source code or configuration files. These hardcoded secrets become part of your Git history and remain accessible even after being removed from the latest commit. This means that addressing a credential leak requires more than deleting the file; you must also revoke and replace the credential to prevent unauthorized access. -Exposed secrets can lead to several types of security incidents: +## How secrets get exposed -**Unauthorized access and usage** -* Leaked cloud provider credentials can be used to provision infrastructure or services on your account -* Database credentials allow access to sensitive customer or organizational data -* Service account tokens provide entry points to production systems +**Secret sprawl** occurs when credentials proliferate across repositories, teams, and systems without centralized management or visibility. This makes it difficult to track which secrets exist, where they're used, and whether they've been exposed. Secrets typically enter repositories through several common patterns. -**Operational and compliance issues** -* Infrastructure can be modified or deleted using leaked credentials -* Data breaches from exposed secrets may result in regulatory penalties under GDPR, CCPA, and other frameworks -* Organizations face costs for incident response, credential rotation, and system remediation +### Development workflows -**Organizational risk** -* Public disclosure of leaked secrets affects customer trust and organizational reputation -* Exposed package registry tokens can be used to publish malicious versions of your software -* Proprietary API keys or service credentials may be exploited +* Hardcoded credentials added during local testing and inadvertently committed +* Secrets in configuration files such as `.env` files or infrastructure-as-code templates +* Example credentials containing real API keys or tokens in documentation, wikis, or README files -## Financial impact of exposed secrets +### Repository management -Secret leakage can result in direct and indirect costs to your organization, ranging from immediate expenses to long-term business consequences. +* Legacy repositories containing forgotten but still-active credentials +* Secrets shared in {% data variables.product.github %} issues, pull request comments, discussions, or gists +* Credentials introduced by external contributors or contractors -**Immediate costs** -* Unauthorized cloud resource usage from leaked API keys can generate charges for compute instances, storage, or data transfer -* Cryptocurrency mining operations on compromised accounts can result in substantial infrastructure bills -* Emergency incident response requires resources for forensic investigation, security audits, and credential rotation across systems +### Version control propagation -**Data breach costs** -* Regulatory fines for data protection violations can reach millions of dollars under GDPR, CCPA, and industry-specific regulations -* Legal costs include investigation, notification requirements, and potential litigation -* Credit monitoring and identity protection services for affected customers +* Secrets persist in Git history even after removal from current code. +* Credentials propagate to forked repositories, backup systems, and CI/CD logs. +* Public repositories with exposed secrets are indexed by search engines and specialized scanning services. -**Operational impact** -* Service disruptions from compromised infrastructure result in lost revenue and productivity -* Engineering time spent responding to security incidents diverts resources from product development -* Increased security tooling and monitoring costs following incidents +## Security risks -**Long-term business impact** -* Customer churn following public disclosure of security incidents -* Increased insurance premiums for cyber liability coverage -* Lost business opportunities due to failed security assessments or compliance audits +Exposed secrets can lead to several types of security incidents. -## Common secret leakage scenarios +### Unauthorized access -Secrets typically enter repositories through several common patterns: +Credential leaks give unauthorized users direct access to your systems. Once exposed, hardcoded secrets can be exploited to: -**Development workflows** -* Credentials hardcoded during local testing and inadvertently committed -* Secrets in configuration files like `.env` files or infrastructure-as-code templates -* Example credentials containing real tokens in documentation, wikis, or README files +* Provision infrastructure or services on your account using leaked cloud provider credentials +* Access sensitive customer or organizational data through compromised database credentials +* Gain entry to production systems via exposed service account tokens -**Repository management** -* Legacy repositories containing forgotten but still-active credentials -* Secrets shared in GitHub issues, pull request comments, discussions, or gists -* Credentials introduced by external contributors or contractors +### Data breaches + +Credential leaks give unauthorized users direct access to your systems, leading to data breaches. Once attackers gain access using exposed credentials, they can exfiltrate sensitive data, modify or delete critical information, and compromise customer trust. Data breaches require immediate incident response, including credential revocation, system remediation, and assessment of the breach's scope and impact. -**Version control challenges** -* Secrets persist in Git history even after removal from current code -* Credentials propagate to forked repositories, backup systems, and logs -* Public repositories with exposed secrets are indexed by search engines and specialized scanning services +### Supply chain attacks + +Exposed package registry tokens can be used to publish malicious versions of your software, affecting downstream users and organizations that depend on your packages. + +## Financial impact + +Exposed secrets can cost your organization money in several ways. + +* **Unexpected cloud bills**: Leaked API keys let attackers use your cloud resources. They can run compute instances, store data, or mine cryptocurrency on your account, generating large bills. +* **Incident response**: Investigating breaches, rotating credentials, and auditing systems takes significant engineering time and resources. +* **Legal costs**: Data breaches can result in fines, legal fees, and notification expenses. +* **Long-term damage**: Lost customers, higher insurance costs, and missed business opportunities after security incidents become public. ## Secret security with {% data variables.product.github %} {% data variables.product.github %} provides tools to help you prevent, detect, and remediate secret leakage: -### 1. Prevent new secrets from being leaked +### 1. Prevent new secrets from being committed -Enable **push protection for repositories** to scan code during `git push` operations and block commits containing detected secrets. This prevents credentials from entering your repositories while providing real-time feedback to developers. +Enable **Push protection** to scan code during `git push` operations and block commits containing detected secrets before they enter your repository. This prevents hardcoded credentials from being added to your codebase and provides real-time feedback to developers at the point of risk, covering both provider patterns for known services and non-provider patterns such as private keys and generic API keys. -Encourage your contributors to enable push protection for their personal accounts (the feature is referred to as "push protection for users") to protect all their pushes to their repositories, forks, and any repositories they contribute to across {% data variables.product.github %}. This allows individual developers to prevent secret leakage without waiting for organization-level policies. +Encourage individual developers to enable push protection for their personal accounts to protect all their pushes across {% data variables.product.github %}, regardless of organization policies. This helps prevent secret sprawl by catching leaked credentials before they reach your repositories. ### 2. Detect existing secrets -Use **{% data variables.product.prodname_secret_scanning %}** to continuously monitor repositories for committed secrets and receive alerts when credentials are detected. This enables you to revoke and rotate compromised credentials quickly. +Use **{% data variables.product.prodname_secret_scanning %}** to continuously monitor your repositories for hardcoded secrets and generate alerts when credentials are detected, enabling you to revoke and rotate compromised credentials quickly. Beyond default detection of provider patterns, you can expand scanning to non-provider patterns and define custom patterns for organization-specific secrets. This helps you gain visibility into secret sprawl across your organization. ## Next steps -To protect your organization from secret leakage:{% ifversion secret-risk-assessment %} -1. Run a free secret risk assessment to understand your current exposure. -{% data variables.secret-scanning.secret-risk-assessment-cta-product %} +To protect your organization from secret leakage: +{% ifversion secret-risk-assessment %} +1. Run a free secret risk assessment to understand your current exposure. {% data variables.secret-scanning.secret-risk-assessment-cta-product %} {% endif %} 1. Enable push protection to prevent new secrets from being committed. -1. Enable {% data variables.product.prodname_secret_scanning %} with a click to begin detecting secret leaks. +1. Enable {% data variables.product.prodname_secret_scanning %} to begin detecting existing secret leaks. 1. Establish secure credential management practices for your development teams. {% ifversion secret-risk-assessment %} diff --git a/content/code-security/getting-started/github-security-features.md b/content/code-security/getting-started/github-security-features.md index 6b60897878a6..c60fb05c307f 100644 --- a/content/code-security/getting-started/github-security-features.md +++ b/content/code-security/getting-started/github-security-features.md @@ -130,7 +130,7 @@ Push protection for users automatically protects you from accidentally committin For accounts on {% ifversion fpt or ghec %}{% data variables.product.prodname_team %} and {% data variables.product.prodname_ghe_cloud %}{% endif %}{% ifversion ghes %} {% data variables.product.prodname_ghe_server %}{% endif %}, you can access additional security features when you purchase **{% data variables.product.prodname_GH_secret_protection %}**. -{% data variables.product.prodname_GH_secret_protection %} includes features that help you detect and prevent secret leaks, such as {% data variables.product.prodname_secret_scanning %} and push protection. +{% data variables.product.prodname_GH_secret_protection %} includes features that help you detect and prevent credential leaks and secret sprawl, such as {% data variables.product.prodname_secret_scanning %} for detecting hardcoded credentials and push protection for blocking them before they reach your repository. These features are available for all repository types. {% ifversion fpt or ghec %}Some of these features are available for public repositories free of charge, meaning that you don't need to purchase {% data variables.product.prodname_GH_secret_protection %} to enable the feature on a public repository.{% endif %} @@ -148,7 +148,7 @@ For information about how you can try {% data variables.product.prodname_GH_secr ### {% data variables.secret-scanning.user_alerts_caps %} -Automatically detect tokens or credentials that have been checked into a repository. You can view alerts for any secrets that {% data variables.product.github %} finds in your code, in the **{% data variables.product.prodname_security_and_quality_tab %}** tab of the repository, so that you know which tokens or credentials to treat as compromised. For more information, see [AUTOTITLE](/code-security/secret-scanning/managing-alerts-from-secret-scanning/about-alerts#about-user-alerts). +Automatically detect hardcoded credentials that have been checked into a repository. You can view alerts for any secrets that {% data variables.product.github %} finds in your code, in the **{% data variables.product.prodname_security_and_quality_tab %}** tab of your repository, so you can respond to credential leaks quickly. For more information, see [AUTOTITLE](/code-security/secret-scanning/introduction/about-secret-scanning). {% data reusables.advanced-security.available-for-public-repos %} @@ -162,7 +162,7 @@ Automatically detect tokens or credentials that have been checked into a reposit ### Push protection -Push protection proactively scans your code, and any repository contributors' code, for secrets during the push process and blocks the push if any secrets are detected. If a contributor bypasses the block, {% data variables.product.github %} creates an alert. For more information, see [AUTOTITLE](/code-security/secret-scanning/introduction/about-push-protection). +Push protection proactively scans your code, and any repository contributors' code, for hardcoded secrets during the push process and blocks the push if any credential leaks are detected. If a contributor bypasses the block, an alert is generated. For more information, see [AUTOTITLE](/code-security/secret-scanning/introduction/about-push-protection). {% data reusables.advanced-security.available-for-public-repos %} diff --git a/content/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/upload-linked-artifacts.md b/content/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/upload-linked-artifacts.md index 418f87119fe0..b0c2c2e32d53 100644 --- a/content/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/upload-linked-artifacts.md +++ b/content/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/upload-linked-artifacts.md @@ -14,7 +14,7 @@ category: The {% data variables.product.virtual_registry %} includes storage records and deployment records for artifacts that you build in your organization. Metadata for each artifact is provided by your organization using one of the following methods: * A workflow containing one of {% data variables.product.company_short %}'s actions for **artifact attestations** -* An integration with the **JFrog Artifactory** or **Microsoft Defender for Cloud** +* An integration with **Dynatrace**, **JFrog Artifactory**, or **{% data variables.product.prodname_microsoft_defender %}** * A custom script using the **artifact metadata REST API** The available methods depend on whether you are uploading a storage record or a deployment record. For more information about record types, see [AUTOTITLE](/code-security/concepts/supply-chain-security/linked-artifacts#which-metadata-is-included). @@ -47,7 +47,20 @@ For artifacts that do not need to be attested and are not stored on JFrog, you c ## Uploading a deployment record -If you store artifacts in **{% data variables.product.prodname_mdc_definition %}**, you can use an integration to automatically sync data to the {% data variables.product.virtual_registry %}. Otherwise, you must set up a custom integration with the **REST API**. +If you monitor deployed workloads with Dynatrace or {% data variables.product.prodname_mdc_definition %}, you can use an integration to automatically sync deployment data to the {% data variables.product.virtual_registry %}. Otherwise, you must set up a custom integration with the REST API. + +### Using the Dynatrace integration + +You can configure Dynatrace to send deployment records to {% data variables.product.github %} for container images running in your Dynatrace-monitored Kubernetes environments. Dynatrace maps deployed images to your repositories, then reports runtime context. + +In addition, deployment records from Dynatrace can include runtime risk context, such as: + +* Public internet exposure +* Sensitive data access + +You can use this context in organization-level alert filtering and in security campaigns to prioritize remediation for alerts that affect internet-exposed or sensitive-data workloads. + +For setup instructions, see [{% data variables.product.prodname_GHAS %} security integration - Get Started](https://docs.dynatrace.com/docs/secure/threat-observability/security-events-ingest/ingest-github-advanced-security#credentials--github-app-based-authentication) in the Dynatrace documentation. ### Using the Microsoft Defender for Cloud integration diff --git a/content/code-security/tutorials/secure-your-organization/prioritize-alerts-in-production-code.md b/content/code-security/tutorials/secure-your-organization/prioritize-alerts-in-production-code.md index b664b7cae8e1..e3fde868a281 100644 --- a/content/code-security/tutorials/secure-your-organization/prioritize-alerts-in-production-code.md +++ b/content/code-security/tutorials/secure-your-organization/prioritize-alerts-in-production-code.md @@ -1,7 +1,7 @@ --- title: Prioritizing Dependabot and code scanning alerts using production context shortTitle: Prioritize alerts in production code -intro: Focus remediation on real risk by targeting {% data variables.product.prodname_dependabot %} and {% data variables.product.prodname_code_scanning %} alerts in artifacts deployed to production, using metadata from external registries like JFrog Artifactory, your own CI/CD workflows, or from {% data variables.product.prodname_microsoft_defender %}. +intro: Focus remediation on real risk by targeting {% data variables.product.prodname_dependabot %} and {% data variables.product.prodname_code_scanning %} alerts in artifacts deployed to production, using metadata from external systems and integrations like Dynatrace, JFrog Artifactory, {% data variables.product.prodname_microsoft_defender %}, or your own CI/CD workflows. versions: fpt: '*' ghec: '*' diff --git a/content/codespaces/index.md b/content/codespaces/index.md index 0ab123a1bce5..698c9987f370 100644 --- a/content/codespaces/index.md +++ b/content/codespaces/index.md @@ -30,8 +30,6 @@ carousels: - /codespaces/reference/security-in-github-codespaces changelog: label: codespaces -product_video: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc' -product_video_transcript: /video-transcripts/transcript-codespaces-your-instant-dev-box-in-the-cloud communityRedirect: name: Provide GitHub Feedback href: 'https://github.com/orgs/community/discussions/categories/codespaces' @@ -42,6 +40,7 @@ redirect_from: /codespaces/developing-in-a-codespace/using-github-codespaces-in-your-jetbrains-ide - /codespaces/reference/using-the-github-codespaces-plugin-for-jetbrains - /codespaces/guides + - /video-transcripts/transcript-codespaces-your-instant-dev-box-in-the-cloud versions: fpt: '*' ghec: '*' diff --git a/content/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam.md b/content/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam.md index 9168f2440a98..025e04ce9ecf 100644 --- a/content/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam.md +++ b/content/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam.md @@ -20,6 +20,8 @@ You can report users that have violated {% data variables.product.prodname_dotco If reported content is enabled for a public repository, you can also report content directly to repository maintainers. +While we encourage using the in-product reporting tools detailed below, you can also report abuse directly through our [contact form](https://support.github.com/contact/report-abuse?category=report-abuse&report=other&report_type=unspecified). + ## Reporting a user {% data reusables.profile.user_profile_page_navigation %} @@ -81,7 +83,6 @@ If reported content is enabled for a public repository, you can also report cont 1. Complete the contact form to tell {% data variables.contact.github_support %} about the contact link's behavior, then click **Send request**. > [!NOTE] -> * In order to get accurate information about the abuse, the abuse report form will direct you to use the in-product abuse report links. If an in-product link is not available, contact {% data variables.contact.contact_support %} to report abuse or report content. > * Users in India can contact {% data variables.product.prodname_dotcom %}'s Grievance Officer for India through [support.github.com/contact/india-grievance-officer](https://support.github.com/contact/india-grievance-officer). ## Further reading diff --git a/content/contributing/writing-for-github-docs/index.md b/content/contributing/writing-for-github-docs/index.md index 235a8f8cc97f..1205a9986a04 100644 --- a/content/contributing/writing-for-github-docs/index.md +++ b/content/contributing/writing-for-github-docs/index.md @@ -14,7 +14,7 @@ children: - /versioning-documentation - /using-markdown-and-liquid-in-github-docs - /using-yaml-frontmatter - - /using-videos-in-github-docs + - /creating-reusable-content - /creating-screenshots - /creating-diagrams-for-github-docs @@ -25,4 +25,5 @@ children: - /templates redirect_from: - /contributing/syntax-and-versioning-for-github-docs + - /contributing/writing-for-github-docs/using-videos-in-github-docs --- diff --git a/content/contributing/writing-for-github-docs/using-videos-in-github-docs.md b/content/contributing/writing-for-github-docs/using-videos-in-github-docs.md deleted file mode 100644 index 386e99bb100e..000000000000 --- a/content/contributing/writing-for-github-docs/using-videos-in-github-docs.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -title: Using videos in GitHub Docs -shortTitle: Use videos -intro: 'This guide explains how to create videos that support user needs for {% data variables.product.prodname_docs %}.' -versions: - fpt: '*' - ghec: '*' - ghes: '*' -category: - - Write and format content ---- - -## About videos in {% data variables.product.prodname_docs %} - -Videos are used rarely in the {% data variables.product.prodname_docs %}. When videos are necessary to provide the best user experience for an article, they are used together with written text. Videos are not a replacement for written content. Videos should never be the only way information is communicated because they are more difficult to keep up to date and are not accessible to everyone. - -Use these guidelines to determine if a video is appropriate to include in an article or on a landing page in the docs. - -If you add a link to a video or embed a video in the {% data variables.product.prodname_docs %}, add the video's metadata to the [Videos in {% data variables.product.prodname_docs %}](https://github.com/github/docs/blob/main/contributing/videos-in-docs.md) file in the `github/docs` repository. - -The Docs team does not create or maintain video content. Videos are purely supplemental to help communicate significant or complex topics, and should be used sparingly because they are not a content type owned by the Docs team. - -## Video checklist - -Use this checklist to quickly determine if a video might be appropriate to add to an article or landing page. - -* Is the video the only way to communicate the information? -* Does {% data variables.product.prodname_dotcom %} own the video? -* Is the video well produced? (See the [Best practices](#best-practices) section for more information.) -* Is the video accessible to the broadest group of users possible? (See the [Accessibility requirements](#accessibility-requirements) section for more information.) -* Is the video less than five minutes long? -* Does the video have a specific audience and purpose in the docs? If it is only relevant to a particular product or feature, you must version it. See the [Versioning](#versioning) section for more information. - -If you answer "no" to any of these items, the video is not suitable for adding to the {% data variables.product.prodname_docs %}. - -### Maintaining videos - -If a video has a maintenance schedule or a team directly responsible for auditing and updating the content if it becomes out of date, you can include the video without any additional steps. - -If the video does not have a maintenance schedule, create an issue with an appropriate target date to review or remove the video. - -## Best practices - -Use these best practices to help determine if a video is well produced and is of a high enough quality to be included in the {% data variables.product.prodname_docs %}. - -Good videos introduce an instructional agenda that includes steps and goals so that someone watching quickly knows what they will learn. Videos are demonstrative, both showing and explaining the relevant steps that are performed. Videos should be engaging and encouraging. Videos must be well produced to be included in the {% data variables.product.prodname_docs %}. A well produced video contains few barriers for people with disabilities, has professional narration (if it is a narrated video), has clear visuals, and comes from a trusted source such as {% data variables.product.prodname_dotcom %} or Microsoft. - -Videos are broadly grouped into three categories: product overviews, feature videos, and tutorials. These descriptions are generalizations of each video type. Some videos might not fit perfectly in one category, but can still be useful without meeting the exact guidelines. - -### Product overviews - -* **Purpose:** Briefly explain what the product is, showcase the main functionality, and get people interested -* **Length:** Less than a minute -* **Possible audiences:** People who want to know if a feature is useful for their goals, people who are new to {% data variables.product.company_short %} and trying to understand what the products do -* **Possible locations in the docs:** Landing pages and guides - -### Feature videos - -* **Purpose:** Supplement conceptual or procedural content -* **Length:** As short as possible, without exceeding five minutes. Break longer content into multiple shorter, focused videos -* **Possible audiences:** People who are learning about or how to use a feature -* **Possible locations in the docs:** Guides, conceptual articles, procedural articles - -### Tutorials - -* **Purpose:** Help novice users get going with a product, drive adoption, or explain complex functionalities -* **Length:** Individual videos should be five minutes or less. Complex topics can have a series of shorter videos spread across an article. Total length should be a maximum of 15 minutes -* **Possible audiences:** New users of features or products -* **Possible locations:** Guides - -## When to use videos - -We might use videos instead of other visuals such as screenshots or diagrams when it is important to show movement or changes in state, like when someone navigates from one screen to another or demos a feature that involves progressing through multiple menus. However, screenshots or text may be sufficient to explain these procedures. - -Videos can also be helpful to introduce features or products where a 30 second video can supplement information that requires multiple paragraphs to write. - -Use videos that explain the value of the procedure or concept that they are showing. - -## When to not use videos - -Do not use videos for features that change quickly and may make videos out of date. Do not use videos that contradict the written content or violate any parts of the [AUTOTITLE](/contributing/style-guide-and-content-model/style-guide#alt-text). Do not use videos that just show a task without explaining or elaborating on the procedure. Videos must be useful and relevant, which includes staying accurate over time. - -## Accessibility requirements - -These are the minimum requirements for a video to be included in the {% data variables.product.prodname_docs %}. If a video violates any of these requirements, it cannot be added to the docs. - -* No flashing or strobe effects -* Must have closed captions. See [Creating video captions](#creating-video-captions) below for more information -* No graphics overlap with where captions appear -* Typography must be legible -* Any overlays must have sufficient contrast ratios -* Any text must be on the screen long enough to be read (the text should appear onscreen for longer than it takes to read it out loud twice) -* Must have proofread descriptive transcripts for what happens scene-by-scene. See [Creating video transcripts](#creating-video-transcripts) below for more information -* Videos do not autoplay - -### Creating video captions - -Videos must have human-generated captions before being added to the Docs site. You can use auto caption technology to help create the captions, but they must be proofread and edited for accuracy by a person. If the video hosting service has a native caption tool, like YouTube, you can use that tool to prepare captions or create a properly formatted `SRT` or `VTT` transcript file to upload with the video. - -Creating captions is part of the process of producing videos that can be accessed by more people, so the owner of a video being added to {% data variables.product.prodname_docs %} should provide captions. - -#### Guidelines for captions - -Where possible, captions should exactly match the words spoken in the video. Do not paraphrase or truncate captions, unless serious time constraints mean it would be difficult for someone to read the captions in the time given. - -Captions must be synchronized to appear at approximately the same time as the audio. Captions should always be timed to appear on screen at the moment the speaker begins talking. For fast speech, where it would be difficult to read captions timed precisely to the audio, you can extend the captions to stay on screen after the speech has finished. - -If a video has multiple speakers, identify the speakers in the captions. Do this by adding the speaker's name, or a descriptive name such as `Developer`, before the start of the sentence. For example: `Jimmy: Hello.`. You only need to do this when the speaker changes, not for every line of dialogue. If it's obvious from the visuals who is speaking, you do not need to identify the speaker. - -Captions must be one or two lines, and no more than 32 characters per line. Put each new sentence on a new line. If you need to break a line mid-sentence, do so at a logical point, for example after commas or before conjunctions like `and` or `but`. - -#### Adding and editing captions on YouTube - -For videos hosted on YouTube, see [Add subtitles and captions](https://support.google.com/youtube/answer/2734796?hl=en&ref_topic=7296214) and [Edit or remove captions](https://support.google.com/youtube/answer/2734705?hl=en&ref_topic=7296214) in the YouTube docs. - -### Creating video transcripts - -For every video linked or embedded in the docs, we must have a descriptive transcript of the video. Transcript articles are formatted like other articles, with YAML frontmatter and Markdown content. To add a transcript to the Docs site, create an article in [`content/video-transcripts`](https://github.com/github/docs/tree/main/content/video-transcripts), and include the transcript as the article's body text. Give the article a filename like `transcript-VIDEO-NAME.md` and a `title` frontmatter property of `Transcript - VIDEO NAME`. Add the article to the `index.md` file in the `video-transcripts` directory. - -Do not use Liquid variables or reusables to replace things like product names in the transcript. The transcript should be faithful to the audio in the video, and we should not change any text in the transcript as a result of updating a variable or reusable after the video was produced. - -Creating transcripts is part of the process of producing videos that can be accessed by more people, so the owner of a video being added to the docs site should provide the content for a transcript. - -You can use captions as the foundation for a transcript. Edit the captions to remove any timestamps and include the relevant information detailed below. A descriptive transcript includes a text version of both audio and visual information needed to understand the content of a video. - -* If a video has multiple speakers, identify the speakers in the transcript. -* If a speaker's gender is known, you can use their preferred pronouns when describing their actions. For example, `She points to the computer screen.` If the speaker's gender is unknown or irrelevant to the visual being described, you can use the singular they pronoun. -* Format the transcript in logical paragraphs, lists, and sections. If it helps people understand the content, you may add headers to sections. Consider how someone would get information from the transcript if they are not also viewing the video. -* Add any onscreen text, relevant visual elements, or non-speech sounds that are not included in the captions. Place these descriptions after the spoken text that accompanies them in the video. Format visual information in brackets. For example, `[Background music plays. The narrator clicks the Code button and then the "+ New codespace" button.]`. -* Add a `product_video` property to the transcript article's YAML frontmatter. The value of the `product_video` property is the YouTube URL of the video. The video's YouTube URL will display as an external link in the transcript article. -* At the end of the transcript, write `End of transcript.` and link to the landing page for the product the video is about using the pattern `For more information about PRODUCT, see the ["Product" documentation](link/to/landing-page).`. - -See [Text Transcript with Description of Visuals](https://www.w3.org/WAI/perspective-videos/captions/#transcript) in the W3C docs for more examples of audio and visual transcriptions. - -#### Linking to transcripts from externally hosted videos - -Add a link to the article with a video's transcript in the description of the video on the platform where it is hosted. For more information, see [Edit video settings](https://support.google.com/youtube/answer/57404?) in the YouTube documentation. - -#### Linking to transcripts for embedded videos - -In any content with an embedded video, add a `product_video_transcript` property below the `product_video` property in the YAML frontmatter. The value of `product_video_transcript` is a link to the transcript article in the `video-transcripts` directory. - -```yaml -title: Example product landing page -product_video: 'https://www.youtube-nocookie.com/embed/URL' -product_video_transcript: /content/video-transcripts/TRANSCRIPT-TITLE -``` - -## Titles for videos - -Titles should be descriptive and follow the guidelines for titles in the content model. For more information, see [AUTOTITLE](/contributing/style-guide-and-content-model/contents-of-a-github-docs-article#titles). - -## Versioning - -If a video is only relevant for specific {% data variables.product.prodname_dotcom %} products (Free, Pro and Team; {% data variables.product.prodname_ghe_server %}; and {% data variables.product.prodname_ghe_cloud %}), the video must be versioned for those products. Use Liquid conditional statements to version the videos appropriately. The Liquid conditional versioning may need to be added when the content is initially created, or may need to be added when the content is updated for a feature update or {% data variables.product.prodname_enterprise %} release. For more information about liquid conditional statements and versioning, see [AUTOTITLE](/contributing/syntax-and-versioning-for-github-docs/versioning-documentation). - -## Video hosting - -Videos must be hosted somewhere that {% data variables.product.prodname_dotcom %} owns and can grant the Docs team access to. Videos should not track users or use cookies. Currently, {% data variables.product.prodname_dotcom %}'s videos are hosted on YouTube and added to the docs using the [Privacy Enhanced Mode](https://support.google.com/youtube/answer/171780?hl=en#zippy=%2Cturn-on-privacy-enhanced-mode) by changing the domain for the embedded URL from `https://www.youtube.com/VIDEO` to `https://www.youtube-nocookie.com/VIDEO`. 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 97f007298c58..40b060957ad9 100644 --- a/content/contributing/writing-for-github-docs/using-yaml-frontmatter.md +++ b/content/contributing/writing-for-github-docs/using-yaml-frontmatter.md @@ -39,8 +39,7 @@ For more information, see [`lib/frontmatter.ts`](https://github.com/github/docs/ * [`changelog`](#changelog) * [`defaultPlatform`](#defaultplatform) * [`defaultTool`](#defaulttool) -* [`learningTracks`](#learningtracks) -* [`includeGuides`](#includeguides) + * [`journeyTracks`](#journeytracks) * [`type`](#type) * [`communityRedirect`](#communityredirect) @@ -226,30 +225,6 @@ defaultPlatform: linux defaultTool: cli ``` -### `learningTracks` - -* Purpose: Render a list of learning tracks on a product's sub-landing page. -* Type: `String`. This should reference learning tracks' names defined in [`data/learning-tracks/*.yml`](https://github.com/github/docs/tree/main/data/learning-tracks). -* Optional - -> [!NOTE] -> The featured track is set by a specific property in the learning tracks YAML. See that [README](https://github.com/github/docs/blob/main/data/learning-tracks/README.md) for details. - -### `includeGuides` - -* Purpose: Render a list of articles, filterable by `type`. Only applicable when used with `layout: product-guides`. -* Type: `Array` -* Optional. - -Example: - -```yaml -includeGuides: - - /actions/guides/about-continuous-integration - - /actions/guides/setting-up-continuous-integration-using-workflow-templates - - /actions/guides/building-and-testing-nodejs - - /actions/guides/building-and-testing-powershell -``` ### `journeyTracks` @@ -338,13 +313,3 @@ The homepage is the main Table of Contents file for the docs site. The homepage `childGroups` is an array of mappings containing a `name` for the group, an optional `icon` for the group, and an array of `children`. The `children` in the array must be present in the `children` frontmatter property. -## Creating new product guides pages - -To create a product guides page (e.g. [{% data variables.product.prodname_actions %} Guide page](/actions/guides)), create or modify an existing markdown file with these specific frontmatter values: - -* Use the product guides page template by referencing `layout: product-guides`. -* Include the learning tracks in [`learningTracks`](#learningtracks). Optional. -* Define which articles to include with [`includeGuides`](#includeguides). Optional. - -If using learning tracks, they need to be defined in [`data/learning-tracks/*.yml`](https://github.com/github/docs/tree/main/data/learning-tracks). -If using `includeGuides`, make sure each of the articles in this list has [`type`](#type) in its frontmatter. diff --git a/content/copilot/reference/copilot-cli-reference/cli-command-reference.md b/content/copilot/reference/copilot-cli-reference/cli-command-reference.md index aa5c5936556a..a460528081f0 100644 --- a/content/copilot/reference/copilot-cli-reference/cli-command-reference.md +++ b/content/copilot/reference/copilot-cli-reference/cli-command-reference.md @@ -17,7 +17,7 @@ redirect_from: | Command | Purpose | |------------------------|----------------------------------------------------| | `copilot` | Launch the interactive user interface. | -| `copilot help [topic]` | Display help information. Help topics include: `config`, `commands`, `environment`, `logging`, and `permissions`. | +| `copilot help [topic]` | Display help information. Help topics include: `config`, `commands`, `environment`, `logging`, `permissions`, and `providers`. | | `copilot init` | Initialize {% data variables.product.prodname_copilot_short %} custom instructions for this repository. | | `copilot update` | Download and install the latest version. | | `copilot version` | Display version information and check for updates. | @@ -74,37 +74,44 @@ redirect_from: | `/add-dir PATH` | Add a directory to the allowed list for file access. | | `/agent` | Browse and select from available agents (if any). | | `/allow-all`, `/yolo` | Enable all permissions (tools, paths, and URLs). | +| `/changelog [SUMMARIZE] [VERSION]` | Display the CLI changelog with an optional AI-generated summary. | | `/clear [PROMPT]`, `/new [PROMPT]` | Start a new conversation. | | `/compact` | Summarize the conversation history to reduce context window usage. | | `/context` | Show the context window token usage and visualization. | +| `/copy` | Copy the last response to the clipboard. | | `/cwd`, `/cd [PATH]` | Change the working directory or display the current directory. | | `/delegate [PROMPT]` | Delegate changes to a remote repository with an AI-generated pull request. | | `/diff` | Review the changes made in the current directory. | | `/exit`, `/quit` | Exit the CLI. | -| `/experimental [on\|off]` | Toggle or turn on/off experimental features. | +| `/experimental [on\|off\|show]` | Toggle, set, or show experimental features. | | `/feedback` | Provide feedback about the CLI. | | `/fleet [PROMPT]` | Enable parallel subagent execution of parts of a task. See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/fleet). | | `/help` | Show the help for interactive commands. | | `/ide` | Connect to an IDE workspace. | | `/init` | Initialize {% data variables.product.prodname_copilot_short %} custom instructions and agentic features for this repository. | +| `/instructions` | View and toggle custom instruction files. | | `/list-dirs` | Display all of the directories for which file access has been allowed. | | `/login` | Log in to {% data variables.product.prodname_copilot_short %}. | | `/logout` | Log out of {% data variables.product.prodname_copilot_short %}. | | `/lsp [show\|test\|reload\|help] [SERVER-NAME]` | Manage the language server configuration. | -| `/mcp [show\|add\|edit\|delete\|disable\|enable] [SERVER-NAME]` | Manage the MCP server configuration. | +| `/mcp [show\|add\|edit\|delete\|disable\|enable\|auth\|reload] [SERVER-NAME]` | Manage the MCP server configuration. | | `/model`, `/models [MODEL]` | Select the AI model you want to use. | +| `/on-air`, `/streamer-mode` | Toggle streamer mode (hides preview model names). | | `/plan [PROMPT]` | Create an implementation plan before coding. | | `/plugin [marketplace\|install\|uninstall\|update\|list] [ARGS...]` | Manage plugins and plugin marketplaces. | -| `/rename NAME` | Rename the current session (alias for `/session rename`). | +| `/pr [view\|create\|fix\|auto]` | Operate on pull requests for the current branch. | +| `/rename [NAME]` | Rename the current session (auto-generates a name if omitted; alias for `/session rename`). | | `/reset-allowed-tools` | Reset the list of allowed tools. | +| `/restart` | Restart the CLI, preserving the current session. | | `/resume [SESSION-ID]` | Switch to a different session by choosing from a list (optionally specify a session ID). | | `/review [PROMPT]` | Run the code review agent to analyze changes. | | `/session [checkpoints [n]\|files\|plan\|rename NAME]` | Show session information and a workspace summary. Use the subcommands for details. | -| `/share [file\|gist] [PATH]` | Share the session to a Markdown file or GitHub gist. | +| `/share [file\|gist] [session\|research] [PATH]` | Share the session to a Markdown file or {% data variables.product.github %} gist. | | `/skills [list\|info\|add\|remove\|reload] [ARGS...]` | Manage skills for enhanced capabilities. | | `/terminal-setup` | Configure the terminal for multiline input support (Shift+Enter and Ctrl+Enter). | | `/theme [show\|set\|list] [auto\|THEME-ID]` | View or configure the terminal theme. | | `/usage` | Display session usage metrics and statistics. | +| `/undo`, `/rewind` | Rewind the last turn and revert file changes. | | `/user [show\|list\|switch]` | Manage the current {% data variables.product.github %} user. | For a complete list of available slash commands enter `/help` in the CLI's interactive interface. @@ -137,7 +144,9 @@ For a complete list of available slash commands enter `/help` in the CLI's inter | `--disable-mcp-server=SERVER-NAME` | Disable a specific MCP server (can be used multiple times). | | `--disable-parallel-tools-execution` | Disable parallel execution of tools (LLM can still make parallel tool calls, but they will be executed sequentially). | | `--disallow-temp-dir` | Prevent automatic access to the system temporary directory. | +| `--effort=LEVEL`, `--reasoning-effort=LEVEL` | Set the reasoning effort level (`low`, `medium`, `high`). | | `--enable-all-github-mcp-tools` | Enable all {% data variables.product.github %} MCP server tools, instead of the default CLI subset. Overrides the `--add-github-mcp-toolset` and `--add-github-mcp-tool` options. | +| `--enable-reasoning-summaries` | Request reasoning summaries for OpenAI models that support it. | | `--excluded-tools=TOOL ...` | These tools will not be available to the model. For multiple tools, use a quoted, comma-separated list. | | `--experimental` | Enable experimental features (use `--no-experimental` to disable). | | `-h`, `--help` | Display help. | @@ -146,19 +155,22 @@ For a complete list of available slash commands enter `/help` in the CLI's inter | `--log-level=LEVEL` | Set the log level (choices: `none`, `error`, `warning`, `info`, `debug`, `all`, `default`). | | `--max-autopilot-continues=COUNT` | Maximum number of continuation messages in autopilot mode (default: unlimited). See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/autopilot). | | `--model=MODEL` | Set the AI model you want to use. | +| `--mouse[=VALUE]` | Enable mouse support in alt screen mode. VALUE can be `on` (default) or `off`. When enabled, the CLI captures mouse events in alt screen mode—scroll wheel, clicks, etc. When disabled, the terminal's native mouse behavior is preserved. Once set the setting is persisted by being written to your configuration file.| | `--no-ask-user` | Disable the `ask_user` tool (the agent works autonomously without asking questions). | | `--no-auto-update` | Disable downloading CLI updates automatically. | | `--no-bash-env` | Disable `BASH_ENV` support for bash shells. | | `--no-color` | Disable all color output. | | `--no-custom-instructions` | Disable loading of custom instructions from `AGENTS.md` and related files. | | `--no-experimental` | Disable experimental features. | +| `--no-mouse` | Disable mouse support. | | `--output-format=FORMAT` | FORMAT can be `text` (default) or `json` (outputs JSONL: one JSON object per line). | | `-p PROMPT`, `--prompt=PROMPT` | Execute a prompt programmatically (exits after completion). | | `--plain-diff` | Disable rich diff rendering (syntax highlighting via the diff tool specified by your git config). | +| `--plugin-dir=DIRECTORY` | Load a plugin from a local directory (can be used multiple times). | | `--resume=SESSION-ID` | Resume a previous interactive session by choosing from a list (optionally specify a session ID). | | `-s`, `--silent` | Output only the agent response (without usage statistics), useful for scripting with `-p`. | | `--screen-reader` | Enable screen reader optimizations. | -| `--secret-env-vars=VAR ...` | An environment variable whose value you want redacted in output. For multiple variables, use a quoted, comma-separated list. The values in the `GITHUB_TOKEN` and `COPILOT_GITHUB_TOKEN` environment variables are redacted by default. | +| `--secret-env-vars=VAR ...` | Redact an environment variable from shell and MCP server environments (can be used multiple times). For multiple variables, use a quoted, comma-separated list. The values in the `GITHUB_TOKEN` and `COPILOT_GITHUB_TOKEN` environment variables are redacted from output by default. | | `--share=PATH` | Share a session to a Markdown file after completion of a programmatic session (default path: `./copilot-session-.md`). | | `--share-gist` | Share a session to a secret {% data variables.product.github %} gist after completion of a programmatic session. | | `--stream=MODE` | Enable or disable streaming mode (mode choices: `on` or `off`). | @@ -280,32 +292,32 @@ Settings cascade from user to repository to local, with more specific scopes ove | Key | Type | Default | Description | |-----|------|---------|-------------| | `allowed_urls` | `string[]` | `[]` | URLs or domains allowed without prompting. | -| `auto_update` | `boolean` | `true` | Automatically download CLI updates. | +| `autoUpdate` | `boolean` | `true` | Automatically download CLI updates. | | `banner` | `"always"` \| `"once"` \| `"never"` | `"once"` | Animated banner display frequency. | -| `bash_env` | `boolean` | `false` | Enable `BASH_ENV` support for bash shells. | +| `bashEnv` | `boolean` | `false` | Enable `BASH_ENV` support for bash shells. | | `beep` | `boolean` | `true` | Play an audible beep when attention is required. | -| `compact_paste` | `boolean` | `true` | Collapse large pastes into compact tokens. | +| `compactPaste` | `boolean` | `true` | Collapse large pastes into compact tokens. | | `custom_agents.default_local_only` | `boolean` | `false` | Only use local custom agents. | | `denied_urls` | `string[]` | `[]` | URLs or domains blocked (takes precedence over `allowed_urls`). | | `experimental` | `boolean` | `false` | Enable experimental features. | | `includeCoAuthoredBy` | `boolean` | `true` | Add a `Co-authored-by` trailer to git commits made by the agent. | | `companyAnnouncements` | `string[]` | `[]` | Custom messages shown randomly on startup. | -| `log_level` | `"none"` \| `"error"` \| `"warning"` \| `"info"` \| `"debug"` \| `"all"` \| `"default"` | `"default"` | Logging verbosity. | +| `logLevel` | `"none"` \| `"error"` \| `"warning"` \| `"info"` \| `"debug"` \| `"all"` \| `"default"` | `"default"` | Logging verbosity. | | `model` | `string` | varies | AI model to use (see the `/model` command). | -| `powershell_flags` | `string[]` | `["-NoProfile", "-NoLogo"]` | Flags passed to PowerShell (`pwsh`) on startup. Windows only. | +| `powershellFlags` | `string[]` | `["-NoProfile", "-NoLogo"]` | Flags passed to PowerShell (`pwsh`) on startup. Windows only. | | `effortLevel` | `string` | `"medium"` | Reasoning effort level for extended thinking (e.g., `"low"`, `"medium"`, `"high"`, `"xhigh"`). Higher levels use more compute. | -| `render_markdown` | `boolean` | `true` | Render Markdown in terminal output. | -| `screen_reader` | `boolean` | `false` | Enable screen reader optimizations. | +| `renderMarkdown` | `boolean` | `true` | Render Markdown in terminal output. | +| `screenReader` | `boolean` | `false` | Enable screen reader optimizations. | | `stream` | `boolean` | `true` | Enable streaming responses. | -| `store_token_plaintext` | `boolean` | `false` | Store authentication tokens in plaintext in the config file when no system keychain is available. | -| `streamer_mode` | `boolean` | `false` | Hide preview model names and quota details (useful when recording). | +| `storeTokenPlaintext` | `boolean` | `false` | Store authentication tokens in plain text in the configuration file when no system keychain is available. | +| `streamerMode` | `boolean` | `false` | Hide preview model names and quota details (useful when demonstrating {% data variables.copilot.copilot_cli_short %}). | | `theme` | `"auto"` \| `"dark"` \| `"light"` | `"auto"` | Terminal color theme. | | `trusted_folders` | `string[]` | `[]` | Folders with pre-granted file access. | | `mouse` | `boolean` | `true` | Enable mouse support in alt screen mode. | | `respectGitignore` | `boolean` | `true` | Exclude gitignored files from the `@` file picker. | | `disableAllHooks` | `boolean` | `false` | Disable all hooks. | | `hooks` | `object` | — | Inline user-level hook definitions. | -| `update_terminal_title` | `boolean` | `true` | Show the current intent in the terminal title. | +| `updateTerminalTitle` | `boolean` | `true` | Show the current intent in the terminal title. | ### Repository settings (`.github/copilot/settings.json`) @@ -316,7 +328,6 @@ Repository settings apply to everyone who works in the repository. Only a subset | `companyAnnouncements` | `string[]` | Replaced—repository takes precedence | Messages shown randomly on startup. | | `enabledPlugins` | `Record` | Merged—repository overrides user for same key | Declarative plugin auto-install. | | `extraKnownMarketplaces` | `Record` | Merged—repository overrides user for same key | Plugin marketplaces available in this repository. | -| `marketplaces` | `Record` | Merged—repository overrides user for same key | Plugin marketplaces (deprecated—use `extraKnownMarketplaces`). | ### Local settings (`.github/copilot/settings.local.json`) @@ -394,12 +405,15 @@ Prompt hooks auto-submit text as if the user typed it. They are only supported o | `sessionEnd` | The session terminates. | No | | `userPromptSubmitted` | The user submits a prompt. | No | | `preToolUse` | Before each tool executes. | Yes — can allow, deny, or modify. | -| `postToolUse` | After each tool completes. | No | +| `postToolUse` | After each tool completes successfully. | Yes — can replace the successful result (SDK programmatic hooks only). | +| `postToolUseFailure` | After a tool completes with a failure. | Yes — can provide recovery guidance via `additionalContext` (exit code `2` for command hooks). | | `agentStop` | The main agent finishes a turn. | Yes — can block and force continuation. | | `subagentStop` | A subagent completes. | Yes — can block and force continuation. | | `subagentStart` | A subagent is spawned (before it runs). Returns `additionalContext` prepended to the subagent's prompt. Supports `matcher` to filter by agent name. | No — cannot block creation. | | `preCompact` | Context compaction is about to begin (manual or automatic). Supports `matcher` to filter by trigger (`"manual"` or `"auto"`). | No — notification only. | +| `permissionRequest` | Before showing a permission dialog to the user, after rule-based checks find no matching allow or deny rule. Supports `matcher` regex on `toolName`. | Yes — can allow or deny programmatically. | | `errorOccurred` | An error occurs during execution. | No | +| `notification` | Fires asynchronously when the CLI emits a system notification (shell completion, agent completion or idle, permission prompts, elicitation dialogs). Fire-and-forget: never blocks the session. Supports `matcher` regex on `notification_type`. | Optional — can inject `additionalContext` into the session. | ### `preToolUse` decision control @@ -418,6 +432,63 @@ The `preToolUse` hook can control tool execution by writing a JSON object to std | `decision` | `"block"`, `"allow"` | `"block"` forces another agent turn using `reason` as the prompt. | | `reason` | string | Prompt for the next turn when `decision` is `"block"`. | +### `permissionRequest` decision control + +The `permissionRequest` hook fires when a tool-level permission dialog is about to be shown. It fires after rule-based permission checks find no matching allow or deny rule. Use it to approve or deny tool calls programmatically—especially useful in pipe mode (`-p`) and CI environments where no interactive prompt is available. + +**Matcher:** Optional regex tested against `toolName`. When set, the hook fires only for matching tool names. + +Output JSON to stdout to control the permission decision: + +| Field | Values | Description | +|-------|--------|-------------| +| `behavior` | `"allow"`, `"deny"` | Whether to approve or deny the tool call. | +| `message` | string | Reason fed back to the LLM when denying. | +| `interrupt` | boolean | When `true` combined with `"deny"`, stops the agent entirely. | + +Return empty output or `{}` to fall through to the default behavior (show the user dialog, or deny in pipe mode). Exit code `2` is treated as a deny; if the hook also outputs JSON on stdout, those fields are merged with the deny decision. + +### `notification` hook + +The `notification` hook fires asynchronously when the CLI emits a system notification. These hooks are fire-and-forget: they never block the session, and any errors are logged and skipped. + +**Input:** + +```typescript +{ + sessionId: string; + timestamp: number; + cwd: string; + hook_event_name: "Notification"; + message: string; // Human-readable notification text + title?: string; // Short title (e.g., "Permission needed", "Shell completed") + notification_type: string; // One of the types listed below +} +``` + +**Notification types:** + +| Type | When it fires | +|------|---------------| +| `shell_completed` | A background (async) shell command finishes | +| `shell_detached_completed` | A detached shell session completes | +| `agent_completed` | A background sub-agent finishes (completed or failed) | +| `agent_idle` | A background agent finishes a turn and enters idle state (waiting for `write_agent`) | +| `permission_prompt` | The agent requests permission to execute a tool | +| `elicitation_dialog` | The agent requests additional information from the user | + +**Output:** + +```typescript +{ + additionalContext?: string; // Injected into the session as a user message +} +``` + +If `additionalContext` is returned, the text is injected into the session as a prepended user message. This can trigger further agent processing if the session is idle. Return `{}` or empty output to take no action. + +**Matcher:** Optional regex on `notification_type`. The pattern is anchored as `^(?:pattern)$`. Omit `matcher` to receive all notification types. + ### Tool names for hook matching | Tool name | Description | @@ -432,7 +503,7 @@ The `preToolUse` hook can control tool execution by writing a JSON object to std | `web_fetch` | Fetch web pages. | | `task` | Run subagent tasks. | -If multiple hooks of the same type are configured, they execute in order. For `preToolUse`, if any hook returns `"deny"`, the tool is blocked. Hook failures (non-zero exit codes or timeouts) are logged and skipped—they never block agent execution. +If multiple hooks of the same type are configured, they execute in order. For `preToolUse`, if any hook returns `"deny"`, the tool is blocked. For `postToolUseFailure` command hooks, exiting with code `2` causes stderr to be returned as recovery guidance for the assistant. Hook failures (non-zero exit codes or timeouts) are logged and skipped—they never block agent execution. ## MCP server configuration @@ -468,8 +539,13 @@ MCP servers provide additional tools to the CLI agent. Configure persistent serv | `headers` | No | HTTP headers. Supports variable expansion. | | `oauthClientId` | No | Static OAuth client ID (skips dynamic registration). | | `oauthPublicClient` | No | Whether the OAuth client is public. Default: `true`. | +| `oidc` | No | Enable OIDC token injection. When `true`, injects a `GITHUB_COPILOT_OIDC_MCP_TOKEN` environment variable (local servers) or a `Bearer` `Authorization` header (remote servers). | | `timeout` | No | Tool call timeout in milliseconds. | +### OAuth re-authentication + +Remote MCP servers that use OAuth may show a `needs-auth` status when a token expires or when a different account is required. Use `/mcp auth ` to trigger a fresh OAuth flow. This opens a browser authentication prompt, allowing you to sign in or switch accounts. After completing the flow, the server reconnects automatically. + ### Filter mapping Control how MCP tool output is processed using the `filterMapping` field in a server's configuration. @@ -537,6 +613,7 @@ Skills are loaded from these locations in priority order (first found wins for d | `~/.claude/skills/` | Personal | Claude-compatible personal location. | | Plugin directories | Plugin | Skills from installed plugins. | | `COPILOT_SKILLS_DIRS` | Custom | Additional directories (comma-separated). | +| (bundled with CLI) | Built-in | Skills shipped with the CLI. Lowest priority—overridable by any other source. | ### Commands (alternative skill format) @@ -551,6 +628,7 @@ Custom agents are specialized AI agents defined in Markdown files. The filename | Agent | Default model | Description | |-------|--------------|-------------| | `code-review` | claude-sonnet-4.5 | High signal-to-noise code review. Analyzes diffs for bugs, security issues, and logic errors. | +| `critic` | complementary model | Adversarial feedback on proposals, designs, and implementations. Identifies weak points and suggests improvements. Experimental—requires `--experimental`. | | `explore` | claude-haiku-4.5 | Fast codebase exploration. Searches files, reads code, and answers questions. Returns focused answers under 300 words. Safe to run in parallel. | | `general-purpose` | claude-sonnet-4.5 | Full-capability agent for complex multi-step tasks. Runs in a separate context window. | | `research` | claude-sonnet-4.6 | Deep research agent. Generates a report based on information in your codebase, in relevant repositories, and on the web. | @@ -591,16 +669,6 @@ When the CLI prompts for permission to execute an operation, you can respond wit Session approvals reset when you run `/clear` or start a new session. -| Flag | Tier | Description | -|------|------|-------------| -| `AUTOPILOT_MODE` | `experimental` | Autonomous operation mode. | -| `BACKGROUND_AGENTS` | `staff` | Run agents in the background. | -| `QUEUED_COMMANDS` | `staff` | Queue commands while the agent is running. | -| `LSP_TOOLS` | `on` | Language Server Protocol tools. | -| `PLAN_COMMAND` | `on` | Interactive planning mode. | -| `AGENTIC_MEMORY` | `on` | Persistent memory across sessions. | -| `CUSTOM_AGENTS` | `on` | Custom agent definitions. | - ## OpenTelemetry monitoring {% data variables.copilot.copilot_cli_short %} can export traces and metrics via [OpenTelemetry](https://opentelemetry.io/) (OTel), giving you visibility into agent interactions, LLM calls, tool executions, and token usage. All signal names and attributes follow the [OTel GenAI Semantic Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/). @@ -632,35 +700,36 @@ The runtime emits a hierarchical span tree for each agent interaction. Each tree #### `invoke_agent` span attributes -Wraps the entire agent invocation: all LLM calls and tool executions for one user message. Span kind: `CLIENT`. - -| Attribute | Description | -|-----------|-------------| -| `gen_ai.operation.name` | `invoke_agent` | -| `gen_ai.provider.name` | Provider (for example, `github`, `anthropic`) | -| `gen_ai.agent.id` | Session identifier | -| `gen_ai.agent.name` | Agent name (subagents only) | -| `gen_ai.agent.description` | Agent description (subagents only) | -| `gen_ai.agent.version` | Runtime version | -| `gen_ai.conversation.id` | Session identifier | -| `gen_ai.request.model` | Requested model | -| `gen_ai.response.model` | Resolved model | -| `gen_ai.response.id` | Last response ID | -| `gen_ai.response.finish_reasons` | `["stop"]` or `["error"]` | -| `gen_ai.usage.input_tokens` | Total input tokens (all turns) | -| `gen_ai.usage.output_tokens` | Total output tokens (all turns) | -| `gen_ai.usage.cache_read.input_tokens` | Cached input tokens read | -| `gen_ai.usage.cache_creation.input_tokens` | Cached input tokens created | -| `github.copilot.turn_count` | Number of LLM round-trips | -| `github.copilot.cost` | Monetary cost | -| `github.copilot.aiu` | AI units consumed | -| `server.address` | Server hostname | -| `server.port` | Server port | -| `error.type` | Error class name (on error) | -| `gen_ai.input.messages` | Full input messages as JSON (content capture only) | -| `gen_ai.output.messages` | Full output messages as JSON (content capture only) | -| `gen_ai.system_instructions` | System prompt content as JSON (content capture only) | -| `gen_ai.tool.definitions` | Tool schemas as JSON (content capture only) | +Wraps the entire agent invocation: all LLM calls and tool executions for one user message. + +* **Top-level sessions** use span kind `CLIENT` (remote service invocation) with `server.address` and `server.port`. +* **Subagent invocations** (for example, explore, task) use span kind `INTERNAL` (in-process) without server attributes. + +| Attribute | Description | Span kind | +|-----------|-------------|-----------| +| `gen_ai.operation.name` | `invoke_agent` | Both | +| `gen_ai.provider.name` | Provider (for example, `github`, `anthropic`) | Both | +| `gen_ai.agent.id` | Session identifier | Both | +| `gen_ai.agent.name` | Agent name (when available) | Both | +| `gen_ai.agent.description` | Agent description (when available) | Both | +| `gen_ai.agent.version` | Runtime version | Both | +| `gen_ai.conversation.id` | Session identifier | Both | +| `gen_ai.request.model` | Requested model | Both | +| `gen_ai.response.finish_reasons` | `["stop"]` or `["error"]` | Both | +| `gen_ai.usage.input_tokens` | Total input tokens (all turns) | Both | +| `gen_ai.usage.output_tokens` | Total output tokens (all turns) | Both | +| `gen_ai.usage.cache_read.input_tokens` | Cached input tokens read | Both | +| `gen_ai.usage.cache_creation.input_tokens` | Cached input tokens created | Both | +| `github.copilot.turn_count` | Number of LLM round-trips | Both | +| `github.copilot.cost` | Monetary cost | Both | +| `github.copilot.aiu` | AI units consumed | Both | +| `server.address` | Server hostname | `CLIENT` only | +| `server.port` | Server port | `CLIENT` only | +| `error.type` | Error class name (on error) | Both | +| `gen_ai.input.messages` | Full input messages as JSON (content capture only) | Both | +| `gen_ai.output.messages` | Full output messages as JSON (content capture only) | Both | +| `gen_ai.system_instructions` | System prompt content as JSON (content capture only) | Both | +| `gen_ai.tool.definitions` | Tool schemas as JSON (content capture only) | Both | #### `chat` span attributes @@ -685,6 +754,7 @@ One span per LLM request. Span kind: `CLIENT`. | `github.copilot.initiator` | Request initiator | | `github.copilot.turn_id` | Turn identifier | | `github.copilot.interaction_id` | Interaction identifier | +| `github.copilot.time_to_first_chunk` | Time to first streaming chunk, in seconds (streaming only) | | `server.address` | Server hostname | | `server.port` | Server port | | `error.type` | Error class name (on error) | @@ -736,9 +806,9 @@ Lifecycle events recorded on the active `chat` or `invoke_agent` span. | `github.copilot.hook.start` | A hook began executing | `github.copilot.hook.type`, `github.copilot.hook.invocation_id` | | `github.copilot.hook.end` | A hook completed successfully | `github.copilot.hook.type`, `github.copilot.hook.invocation_id` | | `github.copilot.hook.error` | A hook failed | `github.copilot.hook.type`, `github.copilot.hook.invocation_id`, `github.copilot.hook.error_message` | -| `github.copilot.session.truncation` | Conversation history was truncated | `github.copilot.token_limit`, `github.copilot.pre_tokens`, `github.copilot.post_tokens`, `github.copilot.tokens_removed`, `github.copilot.messages_removed` | +| `github.copilot.session.truncation` | Conversation history was truncated | `github.copilot.token_limit`, `github.copilot.pre_tokens`, `github.copilot.post_tokens`, `github.copilot.pre_messages`, `github.copilot.post_messages`, `github.copilot.tokens_removed`, `github.copilot.messages_removed`, `github.copilot.performed_by` | | `github.copilot.session.compaction_start` | History compaction began | None | -| `github.copilot.session.compaction_complete` | History compaction completed | `github.copilot.success`, `github.copilot.pre_tokens`, `github.copilot.post_tokens`, `github.copilot.tokens_removed`, `github.copilot.messages_removed` | +| `github.copilot.session.compaction_complete` | History compaction completed | `github.copilot.success`, `github.copilot.pre_tokens`, `github.copilot.post_tokens`, `github.copilot.tokens_removed`, `github.copilot.messages_removed`, `github.copilot.message` (content capture only) | | `github.copilot.skill.invoked` | A skill was invoked | `github.copilot.skill.name`, `github.copilot.skill.path`, `github.copilot.skill.plugin_name`, `github.copilot.skill.plugin_version` | | `github.copilot.session.shutdown` | Session is shutting down | `github.copilot.shutdown_type`, `github.copilot.total_premium_requests`, `github.copilot.lines_added`, `github.copilot.lines_removed`, `github.copilot.files_modified_count` | | `github.copilot.session.abort` | User cancelled the current operation | `github.copilot.abort_reason` | @@ -771,6 +841,23 @@ When content capture is enabled, the following attributes are populated. | `gen_ai.tool.call.arguments` | Tool input arguments | | `gen_ai.tool.call.result` | Tool output | +## Feature flag reference + +Feature flags enable functionality that is not yet generally available. Enable flags via the `COPILOT_CLI_ENABLED_FEATURE_FLAGS` environment variable (comma-separated list) or by using the `/experimental` slash command. + +| Flag | Tier | Description | +|------|------|-------------| +| `CRITIC_AGENT` | experimental | Critic subagent for adversarial feedback on code and designs (Claude and GPT models) | +| `BACKGROUND_SESSIONS` | experimental | Multiple concurrent sessions with background management | +| `MULTI_TURN_AGENTS` | experimental | Multi-turn subagent message passing via `write_agent` | +| `EXTENSIONS` | experimental | Programmatic extensions with custom tools and hooks | +| `QUEUED_COMMANDS` | staff-or-experimental | Queue commands with Ctrl+Enter while the agent runs | +| `PERSISTED_PERMISSIONS` | staff-or-experimental | Persist tool permissions across sessions per location | +| `SESSION_STORE` | staff-or-experimental | SQLite-based session store for cross-session history | +| `COMPUTER_USE` | staff | Built-in computer use MCP server (screen capture and mouse/keyboard control) | +| `copilot-feature-agentic-memory` | on | Persistent memory tools across sessions | +| `COPILOT_SWE_AGENT_BACKGROUND_AGENTS` | on | Background agent task execution | + ## Further reading * [AUTOTITLE](/copilot/how-tos/copilot-cli) diff --git a/content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md b/content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md index 8d9c985f14b9..de014db5afe8 100644 --- a/content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md +++ b/content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md @@ -53,16 +53,16 @@ Common settings include: | `theme` | string | Color theme: `"auto"`, `"dark"`, or `"light"` | | `mouse` | boolean | Enable mouse support in alt screen mode (default: `true`) | | `banner` | string | Animated banner frequency: `"always"`, `"once"`, or `"never"` (default: `"once"`) | -| `render_markdown` | boolean | Render markdown in responses (default: `true`) | -| `screen_reader` | boolean | Enable screen reader optimizations (default: `false`) | -| `auto_update` | boolean | Automatically download CLI updates (default: `true`) | +| `renderMarkdown` | boolean | Render Markdown in responses (default: `true`) | +| `screenReader` | boolean | Enable screen reader optimizations (default: `false`) | +| `autoUpdate` | boolean | Automatically download CLI updates (default: `true`) | | `stream` | boolean | Stream responses token by token (default: `true`) | | `includeCoAuthoredBy` | boolean | Add Co-authored-by to agent-created commits (default: `true`) | | `respectGitignore` | boolean | Exclude gitignored files from the `@` file picker (default: `true`) | | `trusted_folders` | string[] | Folders where read/execute permission has been granted | | `allowed_urls` | string[] | URLs or domains allowed without prompting | | `denied_urls` | string[] | URLs or domains that are always denied | -| `log_level` | string | Log verbosity: `"none"`, `"error"`, `"warning"`, `"info"`, `"debug"`, `"all"`, or `"default"` (default: `"default"`) | +| `logLevel` | string | Log verbosity: `"none"`, `"error"`, `"warning"`, `"info"`, `"debug"`, `"all"`, or `"default"` (default: `"default"`) | | `disableAllHooks` | boolean | Disable all hooks (default: `false`) | | `hooks` | object | Inline user-level hook definitions | diff --git a/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md b/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md index d246d6045812..9327ebbb7ed7 100644 --- a/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md +++ b/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md @@ -92,10 +92,12 @@ For example schemas of the data returned by the APIs, see [AUTOTITLE](/copilot/r | `totals_by_model_feature` / `totals_by_language_model` | Model-specific breakdowns for chat activity (not completions). When {% data variables.copilot.copilot_auto_model_selection_short %} is enabled, activity is attributed to the actual model used rather than appearing as `Auto`. | | `last_known_ide_version` / `last_known_plugin_version` | The most recent IDE and {% data variables.copilot.copilot_chat_short %} extension version detected for each user. | | `daily_active_cli_users` | Number of unique users in the enterprise or organization who used {% data variables.product.prodname_copilot_short %} via the CLI on a given day. This field is **independent** of IDE active user counts and is **not** included in IDE-based active user definitions. Omitted for enterprises or organizations with no CLI usage on that day. | -| `totals_by_cli` | Breakdown of CLI-specific metrics for the enterprise or organization on a given day. Independent of IDE metrics—CLI usage is **not** reflected in other fields such as `totals_by_ide` or `totals_by_feature`. Omitted for enterprises or organizations with no CLI usage on that day. See [{% data variables.copilot.copilot_cli_short %} metrics fields](#copilot-cli-metrics-fields-api-only) below. | +| `totals_by_cli` | Breakdown of CLI-specific metrics for the enterprise, organization, or user on a given day. Independent of IDE metrics—CLI usage is **not** reflected in other fields such as `totals_by_ide` or `totals_by_feature`. Omitted when there's no CLI usage on that day. See [{% data variables.copilot.copilot_cli_short %} metrics fields](#copilot-cli-metrics-fields-api-only) below. | | `used_cli` | Captures whether the user has used {% data variables.copilot.copilot_cli_short %} that day. | -| `used_agent` | Captures whether the user has used IDE agent mode that day. | +| `used_agent` | Captures whether the user has used agent mode in the IDE that day. Does not include {% data variables.copilot.copilot_code-review_short %} activity, which is captured separately in `used_copilot_code_review_active` and `used_copilot_code_review_passive`. | | `used_chat` | Captures whether the user has used IDE chat that day. | +| `used_copilot_code_review_active` | Captures whether the user actively engaged with {% data variables.copilot.copilot_code-review_short %} that day. A user is considered active if they manually requested a {% data variables.product.prodname_copilot_short %} review, or applied a {% data variables.product.prodname_copilot_short %} review suggestion. | +| `used_copilot_code_review_passive` | Captures whether the user had {% data variables.product.prodname_copilot_short %} automatically assigned to review their pull request that day, without actively engaging with the review. | ### {% data variables.copilot.copilot_cli_short %} metrics fields (API only) diff --git a/content/index.md b/content/index.md index 6b26dcf14c07..b612d0033737 100644 --- a/content/index.md +++ b/content/index.md @@ -17,6 +17,7 @@ redirect_from: - /articles - /common-issues-and-questions - /troubleshooting-common-issues + - /video-transcripts - /early-access/github/enforcing-best-practices-with-github-policies - /github/enforcing-best-practices-with-github-policies/index - /early-access/github/enforcing-best-practices-with-github-policies/about-github-policies @@ -83,7 +84,7 @@ children: - desktop - early-access - support - - video-transcripts + - contributing - github-models - nonprofit diff --git a/content/issues/guides.md b/content/issues/guides.md deleted file mode 100644 index fb21208ad6af..000000000000 --- a/content/issues/guides.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Guides for Issues -shortTitle: Guides -intro: 'Learn how you can use {% data variables.product.prodname_github_issues %} to plan and track your work.' -allowTitleToDifferFromFilename: true -layout: product-guides -versions: - fpt: '*' - ghes: '*' - ghec: '*' -includeGuides: - - /issues/tracking-your-work-with-issues/learning-about-issues/quickstart - - /issues/planning-and-tracking-with-projects/learning-about-projects/quickstart-for-projects - - /issues/tracking-your-work-with-issues/learning-about-issues/planning-and-tracking-work-for-your-team-or-project - - /issues/planning-and-tracking-with-projects/learning-about-projects/best-practices-for-projects - - /issues/tracking-your-work-with-issues/using-issues/creating-an-issue - - /issues/planning-and-tracking-with-projects/creating-projects/creating-a-project - - /issues/tracking-your-work-with-issues/using-issues/managing-issue-types-in-an-organization - - /issues/tracking-your-work-with-issues/using-issues/adding-sub-issues - - /issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies - - /issues/tracking-your-work-with-issues/using-issues/filtering-and-searching-issues-and-pull-requests - - /issues/tracking-your-work-with-issues/using-issues/viewing-all-of-your-issues-and-pull-requests - - /issues/using-labels-and-milestones-to-track-work/managing-labels - - /issues/using-labels-and-milestones-to-track-work/creating-and-editing-milestones-for-issues-and-pull-requests - - /issues/planning-and-tracking-with-projects/understanding-fields - - /issues/planning-and-tracking-with-projects/customizing-views-in-your-project - - /issues/planning-and-tracking-with-projects/viewing-insights-from-your-project/creating-charts - - /issues/planning-and-tracking-with-projects/managing-your-project/managing-project-templates-in-your-organization - - /issues/planning-and-tracking-with-projects/automating-your-project/using-the-built-in-automations - - /issues/planning-and-tracking-with-projects/automating-your-project/using-the-api-to-manage-projects - - /issues/planning-and-tracking-with-projects/managing-items-in-your-project/adding-items-to-your-project - - /issues/planning-and-tracking-with-projects/automating-your-project/automating-projects-using-actions - -category: - - Create and work with issues ---- diff --git a/content/issues/index.md b/content/issues/index.md index ce3e549e36a9..b4848a6be389 100644 --- a/content/issues/index.md +++ b/content/issues/index.md @@ -35,9 +35,6 @@ children: - /planning-and-tracking-with-projects - /organizing-your-work-with-project-boards - /using-labels-and-milestones-to-track-work - - /guides -product_video: 'https://www.youtube-nocookie.com/embed/yFQ-p6wMS_Y' -product_video_transcript: /video-transcripts/transcript-using-projects-for-feature-planning redirect_from: - /github/managing-your-work-on-github/managing-your-work-with-issues-and-pull-requests - /github/managing-your-work-on-github/managing-your-work-with-issues @@ -63,4 +60,6 @@ redirect_from: - /disabling-issues - /linking-a-pull-request-to-an-issue - /about-duplicate-issues-and-pull-requests + - /issues/guides + - /video-transcripts/transcript-using-projects-for-feature-planning --- diff --git a/content/migrations/ado/phase-6-follow-up-tasks.md b/content/migrations/ado/follow-up-tasks.md similarity index 97% rename from content/migrations/ado/phase-6-follow-up-tasks.md rename to content/migrations/ado/follow-up-tasks.md index 48dd4b4fbdd6..6985c7d4a0d6 100644 --- a/content/migrations/ado/phase-6-follow-up-tasks.md +++ b/content/migrations/ado/follow-up-tasks.md @@ -1,5 +1,5 @@ --- -title: "Phase 6. Follow-up tasks" +title: "Follow-up tasks" shortTitle: "6. Follow-up tasks" intro: "After each migration has finished, you'll need to complete some additional tasks before the repository is ready for work." versions: @@ -8,6 +8,8 @@ versions: contentType: other category: - Migrate from Azure DevOps +redirect_from: + - /migrations/ado/phase-6-follow-up-tasks --- ## Checking the migration status diff --git a/content/migrations/ado/index.md b/content/migrations/ado/index.md index fe1378d1e1d8..e3493bc004bf 100644 --- a/content/migrations/ado/index.md +++ b/content/migrations/ado/index.md @@ -1,34 +1,41 @@ --- -title: Migrating from Azure DevOps to GitHub +title: Migrating from Azure DevOps shortTitle: Migrate from Azure DevOps -intro: "Plan and execute a migration from Azure DevOps to {% data variables.product.prodname_ghe_cloud %}. This six-phase guide explains how to configure access, migrate, and the follow-up tasks needed to get your repositories ready for work." +intro: Plan and execute a migration from Azure DevOps to {% data variables.product.prodname_ghe_cloud %}. This six-part guide explains how to configure access, migrate, and the follow-up tasks needed to get your repositories ready for work. versions: fpt: '*' ghec: '*' children: - - /phase-1-understand-migrations-from-azure-devops-to-github - - /phase-2-manage-access - - /phase-3-install-and-configure-github-enterprise-importer - - /phase-4-prepare-for-your-migration-from-azure-devops-to-github - - /phase-5-migrate-your-repositories-from-azure-devops-to-github - - /phase-6-follow-up-tasks + - /understand-migrations-from-azure-devops-to-github + - /manage-access + - /install-and-configure-github-enterprise-importer + - /prepare-for-your-migration-from-azure-devops-to-github + - /migrate-your-repositories-from-azure-devops-to-github + - /follow-up-tasks - /key-differences-between-azure-devops-and-github - /use-graphql-to-migrate-repositories-from-azure-devops-to-github-enterprise-cloud - /granting-the-migrator-role redirect_from: - /migrations/using-github-enterprise-importer/migrating-from-azure-devops-to-github-enterprise-cloud +introLinks: + ado_key_differences: /migrations/ado/key-differences-between-azure-devops-and-github contentType: tutorials heroImage: /assets/images/banner-images/hero-3 -layout: bespoke-landing sidebarLink: text: Get started href: /migrations/ado -carousels: - recommended: - - /phase-1-understand-migrations-from-azure-devops-to-github - - /phase-2-manage-access - - /phase-3-install-and-configure-github-enterprise-importer - - /phase-4-prepare-for-your-migration-from-azure-devops-to-github - - /phase-5-migrate-your-repositories-from-azure-devops-to-github - - /phase-6-follow-up-tasks +layout: journey-landing +journeyArticlesHeading: "Steps to completing your migration" +journeyTracks: + - id: ado_migration + title: Run your migration + description: Migrate your repositories from Azure DevOps to GitHub. + guides: + - href: /migrations/ado/understand-migrations-from-azure-devops-to-github + - href: /migrations/ado/manage-access + - href: /migrations/ado/install-and-configure-github-enterprise-importer + - href: /migrations/ado/prepare-for-your-migration-from-azure-devops-to-github + - href: /migrations/ado/migrate-your-repositories-from-azure-devops-to-github + - href: /migrations/ado/follow-up-tasks --- + diff --git a/content/migrations/ado/phase-3-install-and-configure-github-enterprise-importer.md b/content/migrations/ado/install-and-configure-github-enterprise-importer.md similarity index 84% rename from content/migrations/ado/phase-3-install-and-configure-github-enterprise-importer.md rename to content/migrations/ado/install-and-configure-github-enterprise-importer.md index d9361150cb2a..14e2dc30744d 100644 --- a/content/migrations/ado/phase-3-install-and-configure-github-enterprise-importer.md +++ b/content/migrations/ado/install-and-configure-github-enterprise-importer.md @@ -1,5 +1,5 @@ --- -title: "Phase 3. Install and configure GitHub Enterprise Importer" +title: "Install and configure GitHub Enterprise Importer" shortTitle: "3. Configure GitHub Enterprise Importer" intro: "Install the {% data variables.product.prodname_ado2gh_cli %} and configure your environment for the migration." versions: @@ -8,6 +8,8 @@ versions: contentType: other category: - Migrate from Azure DevOps +redirect_from: + - /migrations/ado/phase-3-install-and-configure-github-enterprise-importer --- ## Step 1: Install the {% data variables.product.prodname_ado2gh_cli %} @@ -22,7 +24,7 @@ GitHub Enterprise Importer is a collection of extensions for {% data variables.p Before you can use the {% data variables.product.prodname_ado2gh_cli_short %} to migrate to {% data variables.product.prodname_ghe_cloud %}, you must create {% data variables.product.pat_generic %}s that can access the source and destination organizations, then set the {% data variables.product.pat_generic %}s as environment variables. -1. Make sure you have your {% data variables.product.pat_generic %}s for both {% data variables.product.github %} and Azure DevOps ready. See [AUTOTITLE](/migrations/ado/phase-2-manage-access). +1. Make sure you have your {% data variables.product.pat_generic %}s for both {% data variables.product.github %} and Azure DevOps ready. See [AUTOTITLE](/migrations/ado/phase-2-manage-access?token-backtrack#create-a-personal-access-token-classic-on-github) if you haven't already created a token. 1. Set environment variables for the {% data variables.product.pat_generic %}s, replacing TOKEN in the commands below with the {% data variables.product.pat_generic %}s you previously created. Use `GH_PAT` for the destination organization and `ADO_PAT` for the source organization. * If you're using Terminal, use the `export` command. @@ -40,7 +42,3 @@ Before you can use the {% data variables.product.prodname_ado2gh_cli_short %} to ``` {% data reusables.enterprise-migration-tool.set-target-api-url %} - -## Next steps - -In the next phase, you'll use {% data variables.product.prodname_ghe_cloud %} to prepare for the migration. See [AUTOTITLE](/migrations/ado/phase-4-prepare-for-your-migration-from-azure-devops-to-github). diff --git a/content/migrations/ado/phase-2-manage-access.md b/content/migrations/ado/manage-access.md similarity index 94% rename from content/migrations/ado/phase-2-manage-access.md rename to content/migrations/ado/manage-access.md index d4f636fa9d7d..04f39ab60dff 100644 --- a/content/migrations/ado/phase-2-manage-access.md +++ b/content/migrations/ado/manage-access.md @@ -1,5 +1,5 @@ --- -title: "Phase 2. Manage access" +title: "Manage access" shortTitle: "2. Manage access" intro: "Set up the required access for migrating from Azure DevOps to {% data variables.product.github %}." versions: @@ -9,6 +9,7 @@ contentType: other redirect_from: - /migrations/using-github-enterprise-importer/migrating-from-azure-devops-to-github-enterprise-cloud/managing-access-for-a-migration-from-azure-devops - /migrations/ado/managing-access-for-a-migration-from-azure-devops + - /migrations/ado/phase-2-manage-access category: - Migrate from Azure DevOps --- @@ -55,7 +56,3 @@ If you use your IdP's IP allow list (such as Azure CAP) to restrict access to yo ## Allow migrations to bypass repository rulesets {% data reusables.enterprise-migration-tool.repository-migrations-bypass %} - -## Next steps - -In the next phase, you'll install and configure {% data variables.product.prodname_importer_proper_name %}. See [AUTOTITLE](/migrations/ado/phase-3-install-and-configure-github-enterprise-importer). diff --git a/content/migrations/ado/phase-5-migrate-your-repositories-from-azure-devops-to-github.md b/content/migrations/ado/migrate-your-repositories-from-azure-devops-to-github.md similarity index 93% rename from content/migrations/ado/phase-5-migrate-your-repositories-from-azure-devops-to-github.md rename to content/migrations/ado/migrate-your-repositories-from-azure-devops-to-github.md index b9410b6f6292..a10c74f2d440 100644 --- a/content/migrations/ado/phase-5-migrate-your-repositories-from-azure-devops-to-github.md +++ b/content/migrations/ado/migrate-your-repositories-from-azure-devops-to-github.md @@ -1,5 +1,5 @@ --- -title: "Phase 5. Migrate your repositories from Azure DevOps to Github" +title: "Migrate your repositories from Azure DevOps to GitHub" shortTitle: "5. Migrate repositories" intro: Perform a trial run and then migrate your repositories from Azure DevOps to {% data variables.product.github %}. versions: @@ -12,6 +12,7 @@ redirect_from: - /migrations/using-github-enterprise-importer/migrating-repositories-with-github-enterprise-importer/migrating-repositories-from-azure-devops-to-github-enterprise-cloud - /migrations/using-github-enterprise-importer/migrating-from-azure-devops-to-github-enterprise-cloud/migrating-repositories-from-azure-devops-to-github-enterprise-cloud - /migrations/ado/migrating-repositories-from-azure-devops-to-github-enterprise-cloud + - /migrations/ado/phase-5-migrate-your-repositories-from-azure-devops-to-github category: - Migrate from Azure DevOps --- @@ -70,7 +71,3 @@ If your trial run was successful, and you were able to complete the follow-up ta >[!WARNING] We recommend halting work in the repositories you are migrating. Any changes made during or after the migration will need to be manually migrated. {% data reusables.enterprise-migration-tool.migrate-multiple-repos %} - -## Next steps - -In the next and final phase, you'll perform follow-up tasks, check logs, and get your repositories ready to use. See [AUTOTITLE](/migrations/ado/phase-6-follow-up-tasks). diff --git a/content/migrations/ado/phase-4-prepare-for-your-migration-from-azure-devops-to-github.md b/content/migrations/ado/prepare-for-your-migration-from-azure-devops-to-github.md similarity index 94% rename from content/migrations/ado/phase-4-prepare-for-your-migration-from-azure-devops-to-github.md rename to content/migrations/ado/prepare-for-your-migration-from-azure-devops-to-github.md index edbe1d62be85..f342b2cf9b12 100644 --- a/content/migrations/ado/phase-4-prepare-for-your-migration-from-azure-devops-to-github.md +++ b/content/migrations/ado/prepare-for-your-migration-from-azure-devops-to-github.md @@ -1,5 +1,5 @@ --- -title: "Phase 4. Prepare for your migration from Azure DevOps to GitHub" +title: "Prepare for your migration from Azure DevOps to GitHub" shortTitle: "4. Prepare to migrate" intro: Plan your migration by understanding your timeline, what data will be migrated, and your organizational structure. versions: @@ -8,6 +8,8 @@ versions: contentType: other category: - Migrate from Azure DevOps +redirect_from: + - /migrations/ado/phase-4-prepare-for-your-migration-from-azure-devops-to-github --- ## Determine how much you have to migrate @@ -59,7 +61,3 @@ TEAM-PROJECT-Maintainers | Maintainer TEAM-PROJECT-Admins | Admin To give access to migrated repositories, you can add people to these teams. You can do this manually on {% data variables.product.prodname_dotcom %}, or if you chose to link the teams to Azure Active Directory (AAD) groups during your migration, by managing group membership in AAD. For more information about manually managing team membership, see [AUTOTITLE](/organizations/organizing-members-into-teams/adding-organization-members-to-a-team). - -## Next steps - -In the next phase, you'll perform a dry run and then migrate your repositories. See [AUTOTITLE](/migrations/ado/phase-5-migrate-your-repositories-from-azure-devops-to-github). diff --git a/content/migrations/ado/phase-1-understand-migrations-from-azure-devops-to-github.md b/content/migrations/ado/understand-migrations-from-azure-devops-to-github.md similarity index 92% rename from content/migrations/ado/phase-1-understand-migrations-from-azure-devops-to-github.md rename to content/migrations/ado/understand-migrations-from-azure-devops-to-github.md index fa277903e0ad..1c8415a4f6e5 100644 --- a/content/migrations/ado/phase-1-understand-migrations-from-azure-devops-to-github.md +++ b/content/migrations/ado/understand-migrations-from-azure-devops-to-github.md @@ -1,7 +1,7 @@ --- -title: "Phase 1. Understand migrations from Azure DevOps to GitHub" -shortTitle: "1. Understand migrations" -intro: "{% data variables.product.prodname_importer_proper_name %} can automate migrating from Azure DevOps." +title: Understand migrations from Azure DevOps to GitHub +shortTitle: 1. Understand migrations +intro: '{% data variables.product.prodname_importer_proper_name %} can automate migrating from Azure DevOps.' versions: fpt: '*' ghec: '*' @@ -14,6 +14,7 @@ redirect_from: - /migrations/using-github-enterprise-importer/understanding-github-enterprise-importer/migrating-from-azure-devops-with-github-enterprise-importer - /migrations/using-github-enterprise-importer/migrating-from-azure-devops-to-github-enterprise-cloud/overview-of-a-migration-from-azure-devops-to-github-enterprise-cloud - /migrations/ado/overview-of-a-migration-from-azure-devops-to-github-enterprise-cloud + - /migrations/ado/phase-1-understand-migrations-from-azure-devops-to-github category: - Migrate from Azure DevOps --- @@ -26,6 +27,8 @@ You can only use {% data variables.product.prodname_importer_proper_name %} to m Before you create your enterprise account on {% data variables.product.prodname_dotcom %}, decide whether your enterprise will use {% data variables.product.prodname_emus %}. This affects how your members authenticate and how you manage identities and access. See [AUTOTITLE](/enterprise-cloud@latest/enterprise-onboarding/getting-started-with-your-enterprise/choose-an-enterprise-type). +To learn more about the differences between {% data variables.product.prodname_dotcom %} and Azure DevOps, see [AUTOTITLE](/migrations/ado/key-differences-between-azure-devops-and-github). + ## Support for Azure Pipelines and Azure Boards Both Azure Pipelines and Azure Boards can be fully integrated with your {% data variables.product.prodname_dotcom %} experience. You can configure your enterprise account and Azure DevOps so you can keep using these services while also benefitting from having your repositories hosted on {% data variables.product.prodname_dotcom %}. @@ -61,8 +64,3 @@ There are limits to what {% data variables.product.prodname_importer_proper_name * **Delayed code search functionality:** Re-indexing the search index can take a few hours after a repository is migrated, and code searches may return unexpected results until re-indexing is complete. * **Rulesets configured for your organization can cause migrations to fail:** For example, if you configured a rule that requires email addresses for commit authors to end with `@monalisa.cat`, and the repository you're migrating contains commits that don't comply with this rule, your migration will fail. * **Mannequin content might not be searchable:** Mannequins are placeholder users to which imported content (such as issues, pull requests, comments, etc.) is associated. When you search for content associated with a mannequin, such as assigned issues, the issues may not be found. Once a mannequin is reclaimed, the content should be found via the new owner. - - -## Next steps - -In the next article, you'll decide who will perform the migration and prepare access to both Azure DevOps and {% data variables.product.prodname_ghe_cloud %}. See [AUTOTITLE](/migrations/ado/phase-2-manage-access). diff --git a/content/site-policy/content-removal-policies/github-private-information-removal-policy.md b/content/site-policy/content-removal-policies/github-private-information-removal-policy.md index 95aa3729705f..12f4f5138261 100644 --- a/content/site-policy/content-removal-policies/github-private-information-removal-policy.md +++ b/content/site-policy/content-removal-policies/github-private-information-removal-policy.md @@ -87,7 +87,7 @@ These guidelines are designed to make the processing of requests to remove priva ## How to Submit Your Request -You can submit your request to remove private information via our [contact form](https://support.github.com/contact?tags=docs-private-information). Please include a plain-text version of your request in the body of your message. Sending your request in an attachment may result in processing delays. +You can submit your request to remove private information via our [contact form](https://support.github.com/contact/private-information). Please include a plain-text version of your request in the body of your message. Sending your request in an attachment may result in processing delays. ## Disputes diff --git a/content/sponsors/guides.md b/content/sponsors/guides.md deleted file mode 100644 index a58af27cca15..000000000000 --- a/content/sponsors/guides.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Guides for GitHub Sponsors -shortTitle: Guides -intro: 'Learn how to make the most of {% data variables.product.prodname_sponsors %}.' -allowTitleToDifferFromFilename: true -layout: product-guides -versions: - fpt: '*' - ghec: '*' -includeGuides: - - /sponsors/getting-started-with-github-sponsors/quickstart-for-finding-contributors-to-sponsor - - /sponsors/sponsoring-open-source-contributors/attributing-sponsorships-to-your-organization - - /sponsors/sponsoring-open-source-contributors/managing-your-sponsorship - - /sponsors/receiving-sponsorships-through-github-sponsors/managing-your-payouts-from-github-sponsors - - /sponsors/receiving-sponsorships-through-github-sponsors/setting-up-github-sponsors-for-your-personal-account - - /sponsors/receiving-sponsorships-through-github-sponsors/setting-up-github-sponsors-for-your-organization - - /sponsors/integrating-with-github-sponsors/configuring-webhooks-for-events-in-your-sponsored-account ---- - diff --git a/content/sponsors/index.md b/content/sponsors/index.md index fae7d5208c38..01ce11604bb6 100644 --- a/content/sponsors/index.md +++ b/content/sponsors/index.md @@ -8,6 +8,7 @@ introLinks: redirect_from: - /categories/supporting-the-open-source-community-with-github-sponsors - /github/supporting-the-open-source-community-with-github-sponsors + - /sponsors/guides changelog: label: sponsors layout: discovery-landing @@ -36,5 +37,4 @@ children: - /sponsoring-open-source-contributors - /receiving-sponsorships-through-github-sponsors - /integrating-with-github-sponsors - - /guides --- diff --git a/content/video-transcripts/index.md b/content/video-transcripts/index.md deleted file mode 100644 index 0ca2f9e3013e..000000000000 --- a/content/video-transcripts/index.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Video transcripts -hidden: true -intro: 'Find audio and visual transcripts for videos linked from this site.' -versions: - fpt: '*' - ghec: '*' - ghes: '*' -children: - - transcript-codespaces-your-instant-dev-box-in-the-cloud - - transcript-using-projects-for-feature-planning - - transcript-billing-github-consumption-through-an-azure-subscription ---- \ No newline at end of file diff --git a/content/video-transcripts/transcript-billing-github-consumption-through-an-azure-subscription.md b/content/video-transcripts/transcript-billing-github-consumption-through-an-azure-subscription.md deleted file mode 100644 index 1229bf5ad5c8..000000000000 --- a/content/video-transcripts/transcript-billing-github-consumption-through-an-azure-subscription.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Transcript - "Billing GitHub consumption through an Azure subscription" -intro: Audio and visual transcript. -shortTitle: Billing through Azure -allowTitleToDifferFromFilename: true -product_video: 'https://www.youtube.com/watch?v=Y-f7JKJ4_8Y' -versions: - fpt: '*' - ghec: '*' ---- - -[Title card: "GitHub consolidated billing on Azure."] - -T.J.: Hello, everyone. Thank you for joining us to learn how you can bill your GitHub consumption through Azure. - -During this video, we will share the benefits of billing through Azure, review eligible GitHub products, demo the process step by step, and share resources to help you move forward. - -Let's start with why so many customers are already billing their GitHub consumption through Azure. The key benefits are consolidation, visibility, and commercials. - -From a consolidation standpoint, joint GitHub and Microsoft customers will be able to see all their GitHub consumption on their Azure invoice, eliminating the need for additional invoices and saving time on the procurement process. - -From a visibility perspective, GitHub customers will be able to leverage all the powerful tools and features of the Azure billing portal to gain enhanced visibility into their GitHub consumption spend. - -And finally, if a Microsoft customer has an Azure discount, it will automatically be applied to all future GitHub consumption billed through Azure. - -If a Microsoft customer also has a Microsoft Azure Consumption Commitment, or MACC, all future GitHub consumption will decrement their MACC as well. - -So what GitHub products are eligible for Azure billing? Any GitHub consumption products are eligible today, meaning products that customers pay for based on actual usage, including things like GitHub Copilot, GitHub-hosted actions, larger hosted runners, GitHub Packages and storage, and GitHub Codespaces. - -Historically, GitHub Enterprise and {% data variables.product.prodname_AS %} were only available through an annual license. However, as of August 1, 2024, they are now also available for metered billing through Azure, for additional flexibility and pay-as-you-go pricing. For existing licensed customers, be sure to connect with your GitHub seller to learn more, as certain restrictions may apply. - -[A table shows eligibility for Azure billing and MACCs for the products mentioned. In the table, all products eligible for Azure billing are also eligible for MACCs.] - -T.J.: Now let's jump into a demo to see how to actually turn this on. - -[He shares his screen. He's on the home page of the Microsoft Azure portal.] - -T.J.: The first step in the process is to set up an admin consent workflow to allow your GitHub enterprise owner the ability to request the installation of a new application. This can be done by logging in to the Azure portal and opening up Microsoft Entra ID. - -From here, you'll need to click on "Enterprise Applications", and then under the "Security" section, "Consent and permissions". Finally, click "Admin consent settings". - -From here, you'll need to actually turn this on if it's not already enabled. So I'm going to go ahead and click "Yes". - -[In the settings, under "Users can request admin consent to apps they are unable to consent to", he switches a toggle to "Yes".] - -T.J.: And you'll need to add who has the ability to approve these requests. This would normally be yourself, the global admin, or potentially a team that you're a member of. - -Once you're happy with the settings, you can go ahead and click "Save". From here, you'll need to wait about an hour for these settings to replicate across Azure before you can proceed with the next step. - -Now I'll hand it over to the GitHub enterprise owner to actually request the application be installed. - -[The enterprise owner, Brian, shares his screen. He's looking at an enterprise account on GitHub.] - -Brian: So to start the process of linking an Azure subscription, I need to go to the settings for my enterprise, and underneath "Settings", you'll see there's an option here for billing. Then, under the various settings here, I'm going to go over to "Payment information", and finally "Add Azure Subscription". So I'm gonna click that link. - -[A Microsoft page: "Approval required".] - -Brian: Now once I sign in, it says approval is required from my Azure administrator, so I'm going to say, "Need to have GitHub paid," and let's select "Request approval". - -Now, at this point it says, "Hey, the request has been sent," so I'm gonna go back to my application and it's going to say, "Hey, we're not ready yet." - -[On the enterprise on GitHub, an error message says: "Authentication with Azure failed."] - -Brian: I'm going to go back into my subscription and I'm going to wait to hear from my administrator who will let me know when this is done. - -So now let's switch over back to T.J., and T.J., can you show us the approval process? - -[Back on the Azure portal.] - -T.J.: Your Entra global administrator should receive an email notification to approve a new request. To do so, they can go back into Entra ID, go to "Enterprise applications", and then look for "Admin consent requests" down under the "Activity" section. - -Here you'll see the pending request to install the GitHub Subscription Permission Validator. I can go ahead and click here and review the permissions and consent on behalf of my organization. - -It'll pop up asking me to reauthenticate, and then I can see the exact permissions that it's requesting. In this case, this application is just going to have access to see the users and be able to understand which subscriptions they're in ownership of to make sure that they have permissions to bill against it. - -So if I'm happy with this, I can go ahead and hit "Accept". And that's all I need to do. I can hand it back over to the GitHub enterprise owner to finish the last step. - -[Back in the enterprise on GitHub.] - -Brian: Okay. Well, I checked my email and my administrator approved from the Azure side that we can set up billing. So I'll go back into "Settings", go to "Billing" again, go to "Payment information", "Add Azure Subscription". - -[A dialog displays, labeled "Select a subscription".] - -Brian: Okay. Now that I've been authenticated, we see what subscription I'm hooking up to. I'm going to click it, click "Connect". - -[On the main "Payment information" page, under "Metered billing settings".] - -Brian: With my billing subscription selected, the last thing I need to do is select "Enable metered billing through Azure", and click "Update metered billing settings". - -At this point, the eligible items within my subscriptions, as we described earlier in the slides, will now be billed to the selected subscription. - -T.J.: Thanks so much for joining us today. For future reference, we are also sharing two helpful resources to help you bill GitHub consumption through Azure in the future. And if you have a dedicated GitHub and/or Microsoft account team, please don't hesitate to reach out to them for additional information. Thanks. - -End of transcript. For more information, see [AUTOTITLE](/enterprise-cloud@latest/billing/managing-the-plan-for-your-github-account/connecting-an-azure-subscription) and [Get subscription and tenant IDs in the Azure portal](https://learn.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id) in the Microsoft documentation. diff --git a/content/video-transcripts/transcript-codespaces-your-instant-dev-box-in-the-cloud.md b/content/video-transcripts/transcript-codespaces-your-instant-dev-box-in-the-cloud.md deleted file mode 100644 index 7c74612b76ec..000000000000 --- a/content/video-transcripts/transcript-codespaces-your-instant-dev-box-in-the-cloud.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Transcript - "Codespaces - Your instant dev box in the cloud" -intro: Audio and visual transcript. -shortTitle: Codespaces -allowTitleToDifferFromFilename: true -product_video: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc' -versions: - fpt: '*' - ghec: '*' ---- - -[A developer wearing a blue t-shirt sits at home, behind a mic and a pop shield decorated with GitHub's Octocat logo. The developer shares their screen while still showing on webcam in the upper-right corner. On their screen, they are looking at a GitHub repository called "js-project".] - -Developer: So, today we're gonna run my JavaScript project in GitHub Codespaces. - -[On the repository page, above the list of files, the developer selects the "Code" button to open a dropdown menu. They click "Open with Codespaces", then click "New codespace".] - -Developer: So while we're creating our codespace, you might have the question, "What even is a codespace?" - -[In the browser, the codespace loads. On a white background, the text "Preparing your codespace" is displayed below GitHub's Octocat logo and a VS Code logo.] - -Developer: A codespace is an instant cloud developer environment, where we can run, test, debug, push: everything that we're used to doing in a development environment, but without any of the machine setup we're used to doing locally. - -[The codespace opens in the VS Code web editor in the browser, running with the dark theme. Directories and files from the repository are displayed in the left sidebar. In an integrated terminal, a message says, "Welcome to Codespaces," and lists included runtimes and tools.] - -Developer: So when we create this codespace, you'll see we land in a machine that is already set up, and it has Python, Node, Docker, and even more, so it has tools like Java and Rust and Go and C++. - -But you'll see we landed in the default image. The great thing about Codespaces is you can fully customize your setup, not only for you, but for everyone else who comes along in this repository. So you can have a "config-as-code" setup that allows you to use your own image, or Dockerfile, or Docker Compose. - -So for our app, we're actually just gonna go ahead and start it up. So I'm gonna run `npm start` here, and you'll see that we're told our app is running on port 3000, and we can open it in the browser and connect securely to a forwarded port that has our app fully up and running. - -[A popup is displayed for the forwarded port. On the popup, the developer clicks a button labeled "Open in browser". A new browser tab opens showing a web page titled "Haikus for Mona". On the web page, a cartoon image shows a grinning Mona the Octocat skipping over a puddle with a purple umbrella. Below the image, a haiku: "Rain in Seattle. Don't forget an umbrella, or it will be gloom."] - -Developer: So you'll see here's my Node app, up and running, connected to in a codespace. And this took us about 60 seconds to get set up in Codespaces. - -[The developer scrolls to an image of Mona poking a record player with her tentacle.] - -Developer: So you can imagine this fully customized, to really bring down the setup time for really every repository you have. - -And that's a quick look at GitHub Codespaces. - -End of transcript. For more information about {% data variables.product.prodname_github_codespaces %}, see the [{% data variables.product.prodname_github_codespaces %} documentation](/codespaces). diff --git a/content/video-transcripts/transcript-using-projects-for-feature-planning.md b/content/video-transcripts/transcript-using-projects-for-feature-planning.md deleted file mode 100644 index 4d30bdb86339..000000000000 --- a/content/video-transcripts/transcript-using-projects-for-feature-planning.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Transcript - "Using Projects for feature planning" -intro: Audio and visual transcript. -shortTitle: Projects -allowTitleToDifferFromFilename: true -product_video: 'https://www.youtube-nocookie.com/embed/yFQ-p6wMS_Y' -versions: - fpt: '*' - ghec: '*' - ghes: '*' ---- - -[Fast-paced, techy music plays. On a dark background, GitHub's Octocat logo fades into view. Bright neon lines swirl and branch outwards. Each line leads to something different: a button labeled "Convert to issue"; a pull request merge button marked "Ready"; a comment on a pull request, a project board, and a search field. In a green box, text: "GitHub Issues: Using Projects for feature planning." A pixelated cursor clicks: "Let's go!"] - -[The narrator shares her screen. She's looking at a table layout in a project on GitHub.] - -Narrator: Welcome. Let's take a lap around GitHub Projects and see how it supports you in tracking your feature work from beginning to end. Here we've got our OctoArcade Invaders board, and I'm ready to pick up our next feature. - -[She clicks a tab on the project labeled "Features".] - -Narrator: On our team, we have a simple process where we label the issue as a feature and build a view that focuses on all items with that label. Here we see all issues labeled as a feature. This way I can instantly see that this item is ready to be picked up and go ahead and open it to learn more. - -[She scans her cursor over rows on the table layout and clicks an item called "Player-to-player chat capability." The issue opens as an overlay over the board.] - -Narrator: Given chat is not a small set of work, I've created another product board to track all the various tasks and items that we'll need to do to enable this functionality. - -[She clicks a link in the issue comment, taking her to another project named for the issue. On the "By Area" tab, the project has a table layout, and the rows are separated into groups, like "Design," "Storage," and "Media support," based on labels in the "Area" field.] - -Narrator: In this main view, I've got my work already categorized by this custom field: Area. This is so I can split up my various issues into their appropriate workstreams or buckets. Projects is powerful because you can use these custom fields to add flexible metadata to your issues. You're not just limited by labels or adding a keyword in brackets to your issue title. - -And it's great because I can actually edit this view to group by any of these fields. - -[She clicks a dropdown icon next to the tab name, selects "Group", then scans her cursor over field options like "Status", "Iteration", and "Assignees".] - -Narrator: Now, as we're continuing to flesh out the work for this feature, I can quickly jot down additional items that we need to address by leveraging this "Add" bar. - -[She places her cursor in a text field below the "Design" group, labeled "Add item".] - -Narrator: I'll use this to add several draft issues. For example, our chat client needs a search UI, the ability to edit your friend list, and we definitely need dark mode. - -[She adds a draft issue for each requirement.] - -Narrator: You can see with this "Add" bar, I can write a title, press enter, and keep moving forward to add the next item. It's optimized to help you rapidly take notes on what needs to go into this capability. - -These are draft issues now, but it's as easy as clicking "Convert to issue" to promote them to fully fledged issues in the appropriate repository. - -[She clicks a dropdown icon next to the "Search UI" draft issue, clicks "Convert to issue", and selects from a list of repositories below a search field.] - -Narrator: As development progresses, you'll want to be able to schedule and quickly see status for your work items. This is where our board view shines. - -[She clicks a tab labeled "Iteration plan". Issues, represented as draggable boxes, are arranged into columns for different iterations.] - -Narrator: I've created one here that is grouped by iterations so we can see what's currently planned, what's coming up, and we can quickly drag items to the appropriate iteration to create our plan. - -[She drags items from "No iteration" to "Iteration 3". She clicks a "plus" icon to create a column for "Iteration 4", and drags an item there. Then, she clicks a tab labeled "By Status". Issues are arranged into columns for statuses like "Backlog" and "In Progress".] - -Narrator: Finally, we have our board view grouped by status so we can understand the state of our items at a glance. - -I'm going to add linked pull requests as a visible field here so that my teammates can hop into a PR with just one click to start reviewing and get this issue moved to done. - -[She clicks the dropdown icon next to the tab name, clicks "Title, Assignees, and Status", then selects a checkbox next to a hidden field called "Linked pull requests". In some of the issue boxes, a tag with a pull request icon and number appears.] - -Narrator: We'll be ready to ship the chat feature in no time, with Projects. - -[Blocks cover the screen. The GitHub logo and the word "Issues" appear.] - -End of transcript. For more information about {% data variables.product.prodname_projects_v2 %}, see the [{% data variables.product.prodname_github_issues %} documentation](/issues). diff --git a/data/README.md b/data/README.md index b794ebeefc7d..c782c7121c43 100644 --- a/data/README.md +++ b/data/README.md @@ -32,10 +32,6 @@ Webhook payload JSON files are used in the [`webhook events docs`](../content/de `ui.yml` contains localized strings used in layouts. -## Learning Tracks - -Learning tracks are a collection of articles that help you master a particular subject. See [learning-tracks/README.md](learning-tracks/README.md). - ## Versioning -Many YAML files in this directory's subdirectories, like [features](features), [glossaries](glossaries), [variables](variables), and [learning tracks](learning-tracks), as well as Markdown files within the [reusables](reusables) directory, can include YAML versioning or Liquid versioning within Markdown strings. See the README.md files in the subdirectories for details. +Many YAML files in this directory's subdirectories, like [features](features), [glossaries](glossaries), and [variables](variables), as well as Markdown files within the [reusables](reusables) directory, can include YAML versioning or Liquid versioning within Markdown strings. See the README.md files in the subdirectories for details. diff --git a/data/features/code-security-multi-repo-enablement.yml b/data/features/code-security-multi-repo-enablement.yml deleted file mode 100644 index 9865699abfeb..000000000000 --- a/data/features/code-security-multi-repo-enablement.yml +++ /dev/null @@ -1,6 +0,0 @@ -# Reference: #9212 -# Ref 17108 Advanced Security available to Team plans -versions: - fpt: '*' - ghec: '*' - ghes: '>= 3.10' diff --git a/data/learning-tracks/README.md b/data/learning-tracks/README.md deleted file mode 100644 index 83b6b2bee1fb..000000000000 --- a/data/learning-tracks/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Learning Tracks (aka Learning Paths) - -Learning tracks are a collection of articles that help you master a particular subject. Learning tracks are defined on a per-product basis. For example, see https://docs.github.com/en/actions/guides. - -## How it works - -Learning track data for a product is defined in two places: - -1. A simple array of learning track names is defined in the product guides index page frontmatter. - - For example, in `content/actions/guides/index.md`: - ``` - learningTracks: - - getting_started - - continuous_integration - - continuous_deployment - - deploy_to_the_cloud - - hosting_your_own_runners - - create_actions - ``` - -2. Additional data for each track is defined in a YAML file named for the **product** in the `data` directory. - - For example, in `data/learning-tracks/actions.yml`, each of the items from the content file's `learningTracks` array is represented with additional data such as `title`, `description`, and an array of `guides` links. - -## Versioning - -Versioning for learning tracks is processed at page render time. The code lives in [`lib/learning-tracks.ts`](lib/learning-tracks.ts), which is called by `page.render()`. The processed learning tracks are then rendered by `components/guides`. - -Liquid conditionals do **not** have to be used for versioning in the YAML file for guides. Only the learning track guides that apply to the current version will be rendered automatically. If there aren't any tracks with guides that belong to the current version, the learning tracks section will not render at all. - -Explicit versioning within a product's learning tracks YML data is supported as well. The format and allowed values are the same as the [frontmatter versions property](/content#versions). - -For example: - -``` -learning_track_name: - title: 'Learning track title' - description: 'Learning track description' - versions: - ghes: '>=3.0' - guides: - - /path/to/guide1 - - /path/to/guide2 -``` - -If the `versions` property is not included, it's assumed the track is available in all versions. - -## Schema enforcement - -The schema for validating the learning track YAML lives in [`src/content-linter/lib/learning-tracks-schema.ts`](src/content-linter/lib/learning-tracks-schema.ts) and is exercised by [`tests/content/lint-files.ts`](tests/content/lint-files.ts). diff --git a/data/learning-tracks/admin.yml b/data/learning-tracks/admin.yml deleted file mode 100644 index ae1878552846..000000000000 --- a/data/learning-tracks/admin.yml +++ /dev/null @@ -1,188 +0,0 @@ -deploy_an_instance: - title: Deploy an instance - description: >- - Install {% data variables.product.prodname_ghe_server %} on your platform of - choice and configure SAML authentication. - versions: - ghes: '*' - guides: - - /admin/overview/system-overview - - /admin/installing-your-enterprise-server - - >- - /admin/administering-your-instance/administering-your-instance-from-the-web-ui - - >- - /admin/configuring-settings/configuring-network-settings/configuring-the-hostname-for-your-instance - - /admin/managing-iam/using-saml-for-enterprise-iam -upgrade_your_instance: - title: Upgrade your instance - description: >- - Test upgrades in staging, notify users of maintenance, and upgrade your - instance for the latest features and security updates. - versions: - ghes: '*' - guides: - - >- - /admin/upgrading-your-instance/preparing-to-upgrade/overview-of-the-upgrade-process - - /admin/upgrading-your-instance/preparing-to-upgrade/upgrade-requirements - - >- - /admin/upgrading-your-instance/preparing-to-upgrade/enabling-automatic-update-checks - - >- - /admin/installing-your-enterprise-server/setting-up-a-github-enterprise-server-instance/setting-up-a-staging-instance - - >- - /admin/managing-accounts-and-repositories/communicating-information-to-users-in-your-enterprise/customizing-user-messages-for-your-enterprise - - >- - /admin/administering-your-instance/configuring-maintenance-mode/enabling-and-scheduling-maintenance-mode - - /admin/upgrading-your-instance/preparing-to-upgrade/taking-a-snapshot - - /admin/upgrading-your-instance/performing-an-upgrade -adopting_github_actions_for_your_enterprise_ghec: - title: Adopt GitHub Actions for your enterprise - description: >- - Learn how to plan and implement a rollout of {% data - variables.product.prodname_actions %} in your enterprise. - versions: - ghec: '*' - guides: - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/about-github-actions-for-enterprises - - /actions/get-started/understand-github-actions - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/introducing-github-actions-to-your-enterprise - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/migrating-your-enterprise-to-github-actions - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-cloud - - >- - /actions/reference/security/secure-use - - >- - /billing/concepts/product-billing/github-actions -adopting_github_actions_for_your_enterprise_ghes: - title: Adopt GitHub Actions for your enterprise - description: >- - Learn how to plan and implement a rollout of {% data - variables.product.prodname_actions %} in your enterprise. - versions: - ghes: '*' - guides: - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/about-github-actions-for-enterprises - - /actions/get-started/understand-github-actions - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/introducing-github-actions-to-your-enterprise - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/migrating-your-enterprise-to-github-actions - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-cloud - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-server - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/getting-started-with-self-hosted-runners-for-your-enterprise - - >- - /actions/reference/security/secure-use - - >- - /billing/concepts/product-billing/github-actions -increase_fault_tolerance: - title: Increase the fault tolerance of your instance - description: >- - Back up your developers' code and configure high availability (HA) to ensure - the reliability of {% data variables.product.prodname_ghe_server %} in your - environment. - versions: - ghes: '*' - guides: - - >- - /admin/administering-your-instance/administering-your-instance-from-the-command-line/accessing-the-administrative-shell-ssh - - >- - /admin/backing-up-and-restoring-your-instance/configuring-backups-on-your-instance - - >- - /admin/monitoring-and-managing-your-instance/configuring-high-availability/about-high-availability-configuration - - >- - /admin/monitoring-and-managing-your-instance/configuring-high-availability/creating-a-high-availability-replica - - >- - /admin/configuring-settings/configuring-network-settings/using-github-enterprise-server-with-a-load-balancer -improve_security_of_your_instance: - title: Improve the security of your instance - description: >- - Review network configuration and security features, and harden the instance - running {% data variables.product.prodname_ghe_server %} to protect your - enterprise's data. - versions: - ghes: '*' - guides: - - >- - /admin/configuring-settings/hardening-security-for-your-enterprise/enabling-private-mode - - >- - /admin/configuring-settings/hardening-security-for-your-enterprise/configuring-tls - - >- - /admin/configuring-settings/hardening-security-for-your-enterprise/troubleshooting-tls-errors - - >- - /admin/configuring-settings/hardening-security-for-your-enterprise/enabling-subdomain-isolation - - >- - /admin/administering-your-instance/administering-your-instance-from-the-command-line/accessing-the-administrative-shell-ssh - - /admin/configuring-settings/configuring-network-settings/network-ports - - >- - /admin/configuring-settings/configuring-network-settings/configuring-built-in-firewall-rules - - >- - /admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/best-practices-for-user-security - - >- - /admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/promoting-or-demoting-a-site-administrator -configure_github_actions: - title: Configure {% data variables.product.prodname_actions %} - description: >- - Allow your developers to create, automate, customize, and execute powerful - software development workflows for {% data - variables.location.product_location %} with {% data - variables.product.prodname_actions %}. - versions: - ghes: '*' - guides: - - >- - /admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-server - - >- - /admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-github-actions-in-your-enterprise - - >- - /admin/managing-github-actions-for-your-enterprise/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect - - >- - /admin/managing-github-actions-for-your-enterprise/advanced-configuration-and-troubleshooting/high-availability-for-github-actions - - >- - /admin/managing-github-actions-for-your-enterprise/advanced-configuration-and-troubleshooting/backing-up-and-restoring-github-enterprise-server-with-github-actions-enabled - - >- - /admin/managing-github-actions-for-your-enterprise/advanced-configuration-and-troubleshooting/using-a-staging-environment -configure_github_advanced_security: - title: Configure {% data variables.product.prodname_GHAS %} products - description: >- - Improve the quality and security of your developers' code with {% data - variables.product.prodname_GHAS %} products. - versions: - ghes: '*' - guides: - - >- - /billing/concepts/product-billing/github-advanced-security - - >- - /code-security/how-tos/secure-at-scale/configure-enterprise-security/establish-complete-coverage/enabling-github-advanced-security-for-your-enterprise - - >- - /code-security/how-tos/secure-at-scale/configure-enterprise-security/configure-specific-tools/configuring-code-scanning-for-your-appliance - - >- - /code-security/how-tos/secure-at-scale/configure-enterprise-security/configure-specific-tools/configuring-dependency-review-for-your-appliance - - >- - /code-security/how-tos/secure-at-scale/configure-enterprise-security/configure-specific-tools/configuring-secret-scanning-for-your-appliance - - >- - /admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-code-security-and-analysis-for-your-enterprise -get_started_with_your_enterprise_account: - title: Get started with your enterprise account - description: >- - Get started with your enterprise account to centrally manage multiple - organizations on {% data variables.product.github %}. - versions: - ghes: '*' - ghec: '*' - guides: - - /admin/concepts/enterprise-fundamentals/enterprise-accounts - - /billing/concepts/enterprise-billing/billing-for-enterprises - - >- - /admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/inviting-people-to-manage-your-enterprise - - >- - /admin/managing-accounts-and-repositories/managing-organizations-in-your-enterprise/adding-organizations-to-your-enterprise - - >- - /admin/managing-iam/using-saml-for-enterprise-iam/configuring-saml-single-sign-on-for-your-enterprise - - >- - /admin/concepts/security-and-compliance/enterprise-policies diff --git a/data/learning-tracks/code-security.yml b/data/learning-tracks/code-security.yml deleted file mode 100644 index dfc7d982896b..000000000000 --- a/data/learning-tracks/code-security.yml +++ /dev/null @@ -1,222 +0,0 @@ -security_advisories: - title: Fix and disclose a security vulnerability - description: >- - Using repository security advisories to privately fix a reported - vulnerability and get a CVE. - guides: - - >- - /code-security/concepts/vulnerability-reporting-and-management/about-coordinated-disclosure-of-security-vulnerabilities - - >- - /code-security/concepts/vulnerability-reporting-and-management/about-the-github-advisory-database - - >- - /code-security/concepts/vulnerability-reporting-and-management/about-global-security-advisories - - >- - /code-security/concepts/vulnerability-reporting-and-management/about-repository-security-advisories - - >- - /code-security/tutorials/fix-reported-vulnerabilities/write-security-advisories - - >- - /code-security/how-tos/report-and-fix-vulnerabilities/privately-reporting-a-security-vulnerability - - >- - /code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/managing-privately-reported-security-vulnerabilities - - >- - /code-security/how-tos/report-and-fix-vulnerabilities/configure-vulnerability-reporting/configuring-private-vulnerability-reporting-for-a-repository - - >- - /code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/creating-a-repository-security-advisory - - >- - /code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/adding-a-collaborator-to-a-repository-security-advisory - - >- - /code-security/tutorials/fix-reported-vulnerabilities/collaborate-in-a-fork - - >- - /code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/publishing-a-repository-security-advisory - - >- - /code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/editing-a-repository-security-advisory - - >- - /code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/deleting-a-repository-security-advisory - - >- - /code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/removing-a-collaborator-from-a-repository-security-advisory -dependabot_alerts: - title: Get notifications for insecure dependencies - description: >- - Set up Dependabot to alert you to new vulnerabilities or malware in your - dependencies. - guides: - - /code-security/concepts/supply-chain-security/about-dependabot-alerts - - >- - /repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-security-and-analysis-settings-for-your-repository - - >- - /code-security/how-tos/manage-security-alerts/manage-dependabot-alerts/viewing-and-updating-dependabot-alerts - - >- - /code-security/concepts/supply-chain-security/about-dependabot-auto-triage-rules - - >- - /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configuring-notifications-for-dependabot-alerts - - >- - /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/managing-pull-requests-for-dependency-updates - - >- - /code-security/reference/supply-chain-security/troubleshoot-dependabot/vulnerable-dependency-detection - - >- - /code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors -dependabot_security_updates: - title: Get pull requests to update your vulnerable dependencies - description: >- - Set up Dependabot to create pull requests when new vulnerabilities are - reported. - guides: - - >- - /code-security/concepts/supply-chain-security/about-dependabot-security-updates - - >- - /code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configuring-dependabot-security-updates - - >- - /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configuring-notifications-for-dependabot-alerts - - >- - /repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-security-and-analysis-settings-for-your-repository - - >- - /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/managing-pull-requests-for-dependency-updates - - >- - /code-security/reference/supply-chain-security/troubleshoot-dependabot/vulnerable-dependency-detection -dependency_version_updates: - title: Keep your dependencies up-to-date - description: >- - Use Dependabot to check for new releases and create pull requests to update - your dependencies. - guides: - - >- - /code-security/concepts/supply-chain-security/about-dependabot-version-updates - - >- - /code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configuring-dependabot-version-updates - - >- - /code-security/tutorials/secure-your-dependencies/customizing-dependabot-prs - - >- - /code-security/reference/supply-chain-security/dependabot-options-reference - - >- - /code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/keeping-your-actions-up-to-date-with-dependabot - - >- - /code-security/tutorials/secure-your-dependencies/automating-dependabot-with-github-actions - - >- - /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/listing-dependencies-configured-for-version-updates - - >- - /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configuring-access-to-private-registries-for-dependabot - - >- - /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/guidance-for-the-configuration-of-private-registries-for-dependabot - - >- - /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/removing-dependabot-access-to-public-registries - - >- - /code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/managing-pull-requests-for-dependency-updates - - >- - /code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors -secret_scanning: - title: Scan for secrets - description: >- - Set up secret scanning to guard against accidental check-ins of tokens, - passwords, and other secrets to your repository. - guides: - - /code-security/concepts/secret-security/about-secret-scanning - - /code-security/how-tos/secure-your-secrets/detect-secret-leaks/enabling-secret-scanning-for-your-repository - - /code-security/how-tos/secure-your-secrets/prevent-future-leaks/enabling-push-protection-for-your-repository - - >- - {% ifversion secret-scanning-validity-check-partner-patterns %} - /code-security/how-tos/secure-your-secrets/customize-leak-detection/enabling-validity-checks-for-your-repository{% endif %} - - >- - {% ifversion not fpt - %}/code-security/how-tos/secure-your-secrets/customize-leak-detection/defining-custom-patterns-for-secret-scanning{% - endif %} - - /code-security/how-tos/manage-security-alerts/manage-secret-scanning-alerts - - /code-security/reference/secret-security/supported-secret-scanning-patterns - - >- - {% ifversion secret-scanning-push-protection - %}/code-security/concepts/secret-security/about-push-protection{% - endif %} - - >- - {% ifversion secret-scanning-push-protection-for-users - %}/code-security/how-tos/secure-your-secrets/prevent-future-leaks/manage-user-push-protection{% - endif %} - - >- - {% ifversion secret-scanning-push-protection - %}/code-security/how-tos/secure-your-secrets/work-with-leak-prevention/working-with-push-protection-from-the-command-line{% - endif %} - - >- - {% ifversion secret-scanning-push-protection - %}/code-security/how-tos/secure-your-secrets/work-with-leak-prevention/working-with-push-protection-in-the-github-ui{% - endif %} - - >- - /code-security/reference/secret-security/secret-scanning-detection-scope -security_alerts: - title: Explore and manage security alerts - description: Learn where to find and resolve security alerts. - guides: - - >- - {% ifversion ghec or ghes - %}/code-security/concepts/security-at-scale/about-security-overview {% endif %} - - >- - {% ifversion ghec or ghes - %}/code-security/how-tos/view-and-interpret-data/analyze-organization-data/assessing-adoption-code-security{% - endif %} - - >- - {% ifversion ghec or ghes - %}/code-security/how-tos/view-and-interpret-data/analyze-organization-data/assessing-code-security-risk{% endif - %} - - >- - {% ifversion ghec or ghes - %}/code-security/how-tos/manage-security-alerts/manage-secret-scanning-alerts {% - endif %} - - >- - {% ifversion ghec or ghes - %}/code-security/how-tos/manage-security-alerts/manage-code-scanning-alerts/assessing-code-scanning-alerts-for-your-repository{% - endif %} - - >- - {% ifversion ghec or ghes - %}/code-security/how-tos/manage-security-alerts/manage-code-scanning-alerts/resolving-code-scanning-alerts{% - endif %} - - >- - {% ifversion ghec or ghes - %}/code-security/how-tos/manage-security-alerts/manage-code-scanning-alerts/triaging-code-scanning-alerts-in-pull-requests{% - endif %} - - >- - {% ifversion ghec or ghes - %}/code-security/how-tos/manage-security-alerts/manage-dependabot-alerts/viewing-and-updating-dependabot-alerts{% - endif %} - - >- - {% ifversion ghec or ghes - %}/code-security/concepts/security-at-scale/auditing-security-alerts {% endif %} -code_security_actions: - title: Run code scanning with GitHub Actions - description: >- - Check your default branch and every pull request to keep vulnerabilities and - errors out of your repository. - guides: - - >- - /code-security/concepts/code-scanning/about-code-scanning - - >- - /code-security/how-tos/find-and-fix-code-vulnerabilities/configure-code-scanning/configuring-default-setup-for-code-scanning - - >- - /code-security/reference/code-scanning/workflow-configuration-options - - >- - /code-security/how-tos/find-and-fix-code-vulnerabilities/manage-your-configuration/codeql-code-scanning-for-compiled-languages - - >- - /code-security/tutorials/customize-code-scanning/running-codeql-code-scanning-in-a-container - - /code-security/reference/code-scanning/troubleshoot-analysis-errors - - >- - /code-security/how-tos/find-and-fix-code-vulnerabilities/manage-your-configuration/use-the-tools-status-page-for-code-scanning -code_security_integration: - title: Integrate with code scanning - description: Upload code analysis results from third-party systems to GitHub using SARIF. - guides: - - >- - /code-security/concepts/code-scanning/about-integration-with-code-scanning - - >- - /code-security/how-tos/find-and-fix-code-vulnerabilities/integrate-with-existing-tools/uploading-a-sarif-file-to-github - - >- - /code-security/reference/code-scanning/sarif-files/sarif-support-for-code-scanning - - /rest/code-scanning -end_to_end_supply_chain: - title: End-to-end supply chain - description: >- - How to think about securing your user accounts, your code, and your build - process. - guides: - - >- - /code-security/tutorials/implement-supply-chain-best-practices/end-to-end-supply-chain-overview - - >- - /code-security/tutorials/implement-supply-chain-best-practices/securing-accounts - - /code-security/tutorials/implement-supply-chain-best-practices/securing-code - - >- - /code-security/tutorials/implement-supply-chain-best-practices/securing-builds diff --git a/data/llms-txt-config.yml b/data/llms-txt-config.yml index 7b7c6eecf015..e2ed119b428e 100644 --- a/data/llms-txt-config.yml +++ b/data/llms-txt-config.yml @@ -62,7 +62,6 @@ excluded_categories: - site-policy - subscriptions-and-notifications - support - - video-transcripts # Slugs to exclude (e.g. index pages) excluded_slugs: diff --git a/data/reusables/contributing/content-linter-rules.md b/data/reusables/contributing/content-linter-rules.md index 1ed859c8cb10..ee8f85863d6f 100644 --- a/data/reusables/contributing/content-linter-rules.md +++ b/data/reusables/contributing/content-linter-rules.md @@ -29,7 +29,6 @@ | GHD008 | early-access-references | Files that are not early access should not reference early-access or early-access files | error | feature, early-access | | GHD009 | frontmatter-early-access-references | Files that are not early access should not have frontmatter that references early-access | error | frontmatter, feature, early-access | | GHD010 | frontmatter-hidden-docs | Articles with frontmatter property `hidden` can only be located in specific products | error | frontmatter, feature, early-access | -| GHD011 | frontmatter-video-transcripts | Video transcript must be configured correctly | error | frontmatter, feature, video-transcripts | | GHD012 | frontmatter-schema | Frontmatter must conform to the schema | error | frontmatter, schema | | GHD013 | github-owned-action-references | GitHub-owned action references should not be hardcoded | error | feature, actions | | GHD014 | liquid-data-references-defined | Liquid data or indented data references were found in content that have no value or do not exist in the data directory | error | liquid | diff --git a/data/ui.yml b/data/ui.yml index a9412e255088..a3c072b1cc46 100644 --- a/data/ui.yml +++ b/data/ui.yml @@ -90,7 +90,6 @@ toc: popular: Popular startHere: Start here whats_new: What's new - videos: Videos all_changelogs: All changelog posts pages: article_version: 'Article version' @@ -98,7 +97,6 @@ pages: all_enterprise_releases: All Enterprise Server releases about_versions: About versions permissions_callout_title: Who can use this feature? - video_from_transcript: See video for this transcript copy_as_markdown: Copy as Markdown copy_as_markdown_desc: Use with any LLM view_as_markdown: View as Markdown @@ -285,6 +283,7 @@ product_landing: try_ghas_for_free: Try GitHub Advanced Security for free generate_secret_risk_assessment_report_for_free: Find out how to run a free secret risk assessment plan_your_migration: Plan your migration + ado_key_differences: Key differences between Azure DevOps and GitHub releases: Releases guides: Guides explore_guides: Explore guides @@ -305,32 +304,8 @@ product_landing: docs: docs explore_release_notes: Explore release notes view: View all - view_transcript: View video transcript all_docs: 'All {{ title }} docs' all_content: 'View all {{ title }} content' -product_guides: - learning_paths_title: '{{ name }} learning paths' - start_path: Start learning path - learning_paths_desc: Learning paths are a collection of guides that help you master a particular subject. - more_guides: more guides - load_more: Load more guides - all_guides_title: 'All {{ name }} guides' - filter_instructions: Filter the guide list using these controls - guides_found: - multiple: '{n} guides found' - one: 1 guide found - none: No guides found - guide_types: - get-started: Quickstart - concepts: Concepts - how-tos: How-to guide - tutorials: Tutorial - reference: Reference -learning_track_nav: - prev_guide: Previous - next_guide: Next - more_guides: More guides → - current_progress: '{i} of {n} in learning path' journey_track_nav: prev_article: Previous next_article: Next diff --git a/eslint.config.ts b/eslint.config.ts index a5135704d5c3..8d95bc75dcab 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -215,7 +215,6 @@ export default [ 'src/content-linter/scripts/pretty-print-results.ts', 'src/content-linter/style/base.ts', 'src/content-linter/tests/integration/lint-cli.ts', - 'src/content-linter/tests/learning-track-liquid.ts', 'src/content-linter/tests/lint-files.ts', 'src/content-linter/tests/lint-frontmatter-links.ts', 'src/content-linter/tests/unit/table-column-integrity-simple.ts', diff --git a/package.json b/package.json index b24a4c6afd17..b4de47700214 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,6 @@ "data/variables", "data/glossaries", "data/product-examples", - "data/learning-tracks", "rest-api-description", "semmle-code" ] diff --git a/src/article-api/tests/product-guides-transformer.ts b/src/article-api/tests/product-guides-transformer.ts deleted file mode 100644 index d469e9547942..000000000000 --- a/src/article-api/tests/product-guides-transformer.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { get } from '@/tests/helpers/e2etest' - -const makeURL = (pathname: string): string => - `/api/article/body?${new URLSearchParams({ pathname })}` - -describe('product guides transformer', () => { - test('renders a product guides page with learning tracks', async () => { - // Product guides pages use layout: product-guides - // /en/codespaces/guides is a product guides page - const res = await get(makeURL('/en/codespaces/guides')) - expect(res.statusCode).toBe(200) - expect(res.headers['content-type']).toContain('text/markdown') - - // Should have Links section - expect(res.body).toContain('## Links') - }) - - test('includes guide cards if present', async () => { - const res = await get(makeURL('/en/codespaces/guides')) - expect(res.statusCode).toBe(200) - - // If includeGuides are present, they should appear under Guides - // The rendering depends on what's in the frontmatter - expect(res.body).toMatch(/##|###/) - }) - - test('includes learning tracks if present', async () => { - const res = await get(makeURL('/en/codespaces/guides')) - expect(res.statusCode).toBe(200) - - // Learning tracks should be rendered as sections with their titles - // The actual content depends on frontmatter configuration - expect(res.body).toContain('## Links') - }) -}) diff --git a/src/article-api/transformers/bespoke-landing-transformer.ts b/src/article-api/transformers/bespoke-landing-transformer.ts index 3cc6ae2edfb9..652b69910ff1 100644 --- a/src/article-api/transformers/bespoke-landing-transformer.ts +++ b/src/article-api/transformers/bespoke-landing-transformer.ts @@ -50,8 +50,7 @@ export class BespokeLandingTransformer implements PageTransformer { // Process carousels (each carousel becomes a section) const carousels = bespokePage.carousels ?? bespokePage.rawCarousels if (carousels && typeof carousels === 'object') { - const { default: getLearningTrackLinkData } = - await import('@/learning-track/lib/get-link-data') + const { default: getPageLinkData } = await import('@/frame/lib/get-link-data') for (const [carouselKey, articles] of Object.entries(carousels)) { if (!Array.isArray(articles) || articles.length === 0) continue @@ -66,7 +65,7 @@ export class BespokeLandingTransformer implements PageTransformer { })) } else { // Raw paths that need resolution - const linkData = await getLearningTrackLinkData(articles as string[], context, { + const linkData = await getPageLinkData(articles as string[], context, { title: true, intro: true, }) diff --git a/src/article-api/transformers/discovery-landing-transformer.ts b/src/article-api/transformers/discovery-landing-transformer.ts index 0d38a628269d..3fdf462a95f4 100644 --- a/src/article-api/transformers/discovery-landing-transformer.ts +++ b/src/article-api/transformers/discovery-landing-transformer.ts @@ -50,8 +50,7 @@ export class DiscoveryLandingTransformer implements PageTransformer { // Process carousels (each carousel becomes a section) const carousels = discoveryPage.carousels ?? discoveryPage.rawCarousels if (carousels && typeof carousels === 'object') { - const { default: getLearningTrackLinkData } = - await import('@/learning-track/lib/get-link-data') + const { default: getPageLinkData } = await import('@/frame/lib/get-link-data') for (const [carouselKey, articles] of Object.entries(carousels)) { if (!Array.isArray(articles) || articles.length === 0) continue @@ -66,7 +65,7 @@ export class DiscoveryLandingTransformer implements PageTransformer { })) } else { // Raw paths that need resolution - const linkData = await getLearningTrackLinkData(articles as string[], context, { + const linkData = await getPageLinkData(articles as string[], context, { title: true, intro: true, }) @@ -94,12 +93,11 @@ export class DiscoveryLandingTransformer implements PageTransformer { // Intro links (getting started) const rawIntroLinks = discoveryPage.introLinks ?? discoveryPage.rawIntroLinks if (rawIntroLinks) { - const { default: getLearningTrackLinkData } = - await import('@/learning-track/lib/get-link-data') + const { default: getPageLinkData } = await import('@/frame/lib/get-link-data') const links = await Promise.all( Object.values(rawIntroLinks).map(async (href): Promise => { if (typeof href === 'string') { - const linkData = await getLearningTrackLinkData(href, context) + const linkData = await getPageLinkData(href, context) if (Array.isArray(linkData) && linkData.length > 0) { const item = linkData[0] return { href: item.href || '', title: item.title || '', intro: item.intro || '' } diff --git a/src/article-api/transformers/index.ts b/src/article-api/transformers/index.ts index 1c1b90f7b4cb..cc8ba96f44e3 100644 --- a/src/article-api/transformers/index.ts +++ b/src/article-api/transformers/index.ts @@ -14,7 +14,6 @@ import { BespokeLandingTransformer } from './bespoke-landing-transformer' import { JourneyLandingTransformer } from './journey-landing-transformer' import { CategoryLandingTransformer } from './category-landing-transformer' import { DiscoveryLandingTransformer } from './discovery-landing-transformer' -import { ProductGuidesTransformer } from './product-guides-transformer' import { ProductLandingTransformer } from './product-landing-transformer' import { SearchPageTransformer } from './search-page-transformer' import { ArticleTransformer } from './article-transformer' @@ -40,7 +39,6 @@ transformerRegistry.register(new BespokeLandingTransformer()) transformerRegistry.register(new JourneyLandingTransformer()) transformerRegistry.register(new CategoryLandingTransformer()) transformerRegistry.register(new DiscoveryLandingTransformer()) -transformerRegistry.register(new ProductGuidesTransformer()) transformerRegistry.register(new ProductLandingTransformer()) transformerRegistry.register(new SearchPageTransformer()) // ArticleTransformer is the catch-all — must be registered last. diff --git a/src/article-api/transformers/product-guides-transformer.ts b/src/article-api/transformers/product-guides-transformer.ts deleted file mode 100644 index 3b386cba8e86..000000000000 --- a/src/article-api/transformers/product-guides-transformer.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { Context, Page } from '@/types' -import type { PageTransformer, TemplateData, Section, LinkGroup, LinkData } from './types' -import { renderContent } from '@/content-render/index' -import { loadTemplate } from '@/article-api/lib/load-template' - -interface ProcessedLink { - href: string - title?: string - intro?: string -} - -interface LearningTrack { - title: string - guides: ProcessedLink[] -} - -/** - * ProductGuidesPage extends Page with optional guide and learning track fields. - * - includeGuides/rawIncludeGuides: Curated list of guide articles (processed objects vs raw paths) - * - learningTracks/rawLearningTracks: Grouped tutorials (processed objects vs raw track IDs) - */ -interface ProductGuidesPage extends Page { - includeGuides?: ProcessedLink[] - rawIncludeGuides?: string[] - learningTracks?: LearningTrack[] - rawLearningTracks?: string[] -} - -/** - * Transforms product-guides pages into markdown format. - * Handles includeGuides (curated articles) and learningTracks (grouped tutorials). - */ -export class ProductGuidesTransformer implements PageTransformer { - templateName = 'landing-page.template.md' - - canTransform(page: Page): boolean { - return page.layout === 'product-guides' - } - - async transform(page: Page, pathname: string, context: Context): Promise { - const templateData = await this.prepareTemplateData(page, pathname, context) - const templateContent = loadTemplate(this.templateName) - - return await renderContent(templateContent, { - ...context, - ...templateData, - markdownRequested: true, - }) - } - - private async prepareTemplateData( - page: Page, - pathname: string, - context: Context, - ): Promise { - const guidesPage = page as ProductGuidesPage - const sections: Section[] = [] - const groups: LinkGroup[] = [] - - // Include guides - const includeGuidesData = guidesPage.includeGuides ?? guidesPage.rawIncludeGuides - if (includeGuidesData && includeGuidesData.length > 0) { - const { default: getLinkData } = await import('@/learning-track/lib/get-link-data') - - const isProcessed = typeof includeGuidesData[0] === 'object' - - let processedLinks: ProcessedLink[] - if (isProcessed) { - processedLinks = includeGuidesData as ProcessedLink[] - } else { - processedLinks = - (await getLinkData(includeGuidesData as string[], context, { - title: true, - intro: true, - })) || [] - } - - const links: LinkData[] = (processedLinks || []).map((item) => ({ - href: item.href, - title: item.title || '', - intro: item.intro || '', - })) - - const validLinks = links.filter((l) => l.href) - if (validLinks.length > 0) { - groups.push({ title: 'Guides', links: validLinks }) - } - } - - // Learning tracks - const learningTracksData = guidesPage.learningTracks ?? guidesPage.rawLearningTracks - if (learningTracksData && learningTracksData.length > 0) { - let processedTracks: LearningTrack[] - if (Array.isArray(guidesPage.learningTracks) && guidesPage.learningTracks.length > 0) { - processedTracks = guidesPage.learningTracks - } else { - const { default: processLearningTracks } = - await import('@/learning-track/lib/process-learning-tracks') - const { learningTracks } = await processLearningTracks( - learningTracksData as string[], - context, - ) - processedTracks = learningTracks - } - - for (const track of processedTracks) { - if (!track.guides || !Array.isArray(track.guides)) continue - - const links: LinkData[] = track.guides.map((guide) => ({ - href: guide.href, - title: guide.title || '', - intro: guide.intro || '', - })) - - if (links.length > 0) { - groups.push({ title: track.title, links }) - } - } - } - - if (groups.length > 0) { - sections.push({ - title: 'Links', - groups, - }) - } - - const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' - const title = await page.renderTitle(context, { unwrap: true }) - - return { - title, - intro, - sections, - } - } -} diff --git a/src/article-api/transformers/product-landing-transformer.ts b/src/article-api/transformers/product-landing-transformer.ts index c5350620c15a..510491362fa2 100644 --- a/src/article-api/transformers/product-landing-transformer.ts +++ b/src/article-api/transformers/product-landing-transformer.ts @@ -20,7 +20,7 @@ interface PageWithChildren extends Page { /** * Transforms product-landing pages into markdown format. - * Handles featured links (startHere, popular, videos), guide cards, + * Handles featured links (startHere, popular), guide cards, * article grids with category filtering, and children listings. */ export class ProductLandingTransformer implements PageTransformer { @@ -56,8 +56,7 @@ export class ProductLandingTransformer implements PageTransformer { // Process carousels (each carousel becomes a section) const carousels = productPage.carousels ?? productPage.rawCarousels if (carousels && typeof carousels === 'object') { - const { default: getLearningTrackLinkData } = - await import('@/learning-track/lib/get-link-data') + const { default: getPageLinkData } = await import('@/frame/lib/get-link-data') for (const [carouselKey, articles] of Object.entries(carousels)) { if (!Array.isArray(articles) || articles.length === 0) continue @@ -72,7 +71,7 @@ export class ProductLandingTransformer implements PageTransformer { })) } else { // Raw paths that need resolution - const linkData = await getLearningTrackLinkData(articles as string[], context, { + const linkData = await getPageLinkData(articles as string[], context, { title: true, intro: true, }) @@ -97,13 +96,12 @@ export class ProductLandingTransformer implements PageTransformer { } } - // Featured links (startHere, popular, videos, etc.) + // Featured links (startHere, popular, etc.) const rawFeaturedLinks = productPage.featuredLinks if (rawFeaturedLinks) { - const { default: getLearningTrackLinkData } = - await import('@/learning-track/lib/get-link-data') + const { default: getPageLinkData } = await import('@/frame/lib/get-link-data') - const featuredKeys = ['startHere', 'popular', 'videos'] + const featuredKeys = ['startHere', 'popular'] const featuredGroups: LinkGroup[] = [] for (const key of featuredKeys) { @@ -112,33 +110,17 @@ export class ProductLandingTransformer implements PageTransformer { const sectionTitle = this.getSectionTitle(key) - let resolvedLinks: LinkData[] - - if (key === 'videos') { - // Videos are external URLs with title and href properties - const videoLinks = await Promise.all( - links.map(async (link) => { - if (typeof link === 'object' && link.href) { - const title = await renderContent(link.title, context, { textOnly: true }) - return title ? { href: link.href, title, intro: link.intro || '' } : null - } - return null - }), - ) - resolvedLinks = videoLinks.filter((l) => l !== null) as LinkData[] - } else { - // Other featuredLinks are page hrefs that need Liquid evaluation - const stringLinks = links.map((item) => (typeof item === 'string' ? item : item.href)) - const linkData = await getLearningTrackLinkData(stringLinks, context, { - title: true, - intro: true, - }) - resolvedLinks = (linkData || []).map((item) => ({ - href: item.href, - title: item.title || '', - intro: item.intro || '', - })) - } + // featuredLinks are page hrefs that need Liquid evaluation + const stringLinks = links.map((item) => (typeof item === 'string' ? item : item.href)) + const linkData = await getPageLinkData(stringLinks, context, { + title: true, + intro: true, + }) + const resolvedLinks = (linkData || []).map((item) => ({ + href: item.href, + title: item.title || '', + intro: item.intro || '', + })) const validLinks = resolvedLinks.filter((l) => l.href) if (validLinks.length > 0) { @@ -292,7 +274,6 @@ export class ProductLandingTransformer implements PageTransformer { startHere: 'Start here', guideCards: 'Guides', popular: 'Popular', - videos: 'Videos', } return map[key] || key } diff --git a/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts b/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts index ef00f7cbb5ec..8ffb098d5adc 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts @@ -16,12 +16,7 @@ export const frontmatterHiddenDocs = { if (fm.hasExperimentalAlternative) return // Hidden docs can be located in these content directories: - const allowedProductPaths = [ - 'content/early-access', - 'content/site-policy', - 'content/search', - 'content/video-transcripts', - ] + const allowedProductPaths = ['content/early-access', 'content/site-policy', 'content/search'] if (allowedProductPaths.some((allowedPath) => params.name.includes(allowedPath))) return diff --git a/src/content-linter/lib/linting-rules/frontmatter-video-transcripts.ts b/src/content-linter/lib/linting-rules/frontmatter-video-transcripts.ts deleted file mode 100644 index 2d5d7fe0fc86..000000000000 --- a/src/content-linter/lib/linting-rules/frontmatter-video-transcripts.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { addError } from 'markdownlint-rule-helpers' -import path from 'path' - -import { getFrontmatter } from '../helpers/utils' -import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types' - -interface Frontmatter { - product_video?: string - product_video_transcript?: string - title?: string - layout?: string - [key: string]: unknown -} - -export const frontmatterVideoTranscripts: Rule = { - names: ['GHD011', 'frontmatter-video-transcripts'], - description: 'Video transcript must be configured correctly', - tags: ['frontmatter', 'feature', 'video-transcripts'], - function: (params: RuleParams, onError: RuleErrorCallback) => { - const filepath = params.name - - const fm = getFrontmatter(params.lines) as Frontmatter | null - if (!fm) return - - const isTranscriptContent = - filepath.includes('video-transcripts') && path.basename(filepath) !== 'index.md' - - // Video transcripts must have specific frontmatter properties - if (isTranscriptContent) { - if (!fm.product_video) { - const lineNumber = 1 - addError( - onError, - lineNumber, - 'Video transcripts must contain an reference to the video being transcribed. Ensure the frontmatter property `product_video` is set to the path of the video.', - null, // No context because the property is missing - null, // No range for missing line - null, // No fix possible - ) - } - if (fm.title && !fm.title.startsWith('Transcript - ')) { - const lineNumber = params.lines.findIndex((line) => line.startsWith('title:')) + 1 - const lineContent = params.lines[lineNumber - 1] - addError( - onError, - lineNumber, - 'Video transcript pages must prepend the frontmatter title property with "Transcript - ".', - lineContent, - [1, lineContent.length], - null, // No fix possible - ) - } - } - - // A landing page that defines either a product_video or - // product_video_transcript frontmatter must define both - if (fm.layout === 'product-landing' && (fm.product_video || fm.product_video_transcript)) { - const lineNumber = - params.lines.findIndex( - (line) => - line.startsWith('product_video_transcript:') || line.startsWith('product_video:'), - ) + 1 - const lineContent = params.lines[lineNumber - 1] - if ( - !fm.product_video_transcript || - !fm.product_video_transcript.startsWith('/video-transcripts/') - ) { - addError( - onError, - lineNumber, - 'Videos on product landing pages must contain a video transcript. Ensure the frontmatter property `product_video_transcript` is set to the path of the transcript in the video-transcript directory.', - lineContent, - [1, lineContent.length], - null, // No fix possible - ) - } - if (!fm.product_video) { - addError( - onError, - lineNumber, - 'Video transcripts on product landing pages must have a product video. Ensure the frontmatter property `product_video` is set to the path of the video.', - lineContent, - [1, lineContent.length], - null, // No fix possible - ) - } - } - }, -} diff --git a/src/content-linter/lib/linting-rules/index.ts b/src/content-linter/lib/linting-rules/index.ts index 47c182b64075..d2d94ce7ede6 100644 --- a/src/content-linter/lib/linting-rules/index.ts +++ b/src/content-linter/lib/linting-rules/index.ts @@ -13,7 +13,6 @@ import { frontmatterEarlyAccessReferences, } from '@/content-linter/lib/linting-rules/early-access-references' import { frontmatterHiddenDocs } from '@/content-linter/lib/linting-rules/frontmatter-hidden-docs' -import { frontmatterVideoTranscripts } from '@/content-linter/lib/linting-rules/frontmatter-video-transcripts' import { yamlScheduledJobs } from '@/content-linter/lib/linting-rules/yaml-scheduled-jobs' import { internalLinksOldVersion } from '@/content-linter/lib/linting-rules/internal-links-old-version' import { hardcodedDataVariable } from '@/content-linter/lib/linting-rules/hardcoded-data-variable' @@ -85,7 +84,6 @@ export const gitHubDocsMarkdownlint = { earlyAccessReferences, // GHD008 frontmatterEarlyAccessReferences, // GHD009 frontmatterHiddenDocs, // GHD010 - frontmatterVideoTranscripts, // GHD011 frontmatterSchema, // GHD012 githubOwnedActionReferences, // GHD013 liquidDataReferencesDefined, // GHD014 diff --git a/src/content-linter/style/github-docs.ts b/src/content-linter/style/github-docs.ts index c8f9ce3253e5..a874ee6ff585 100644 --- a/src/content-linter/style/github-docs.ts +++ b/src/content-linter/style/github-docs.ts @@ -209,12 +209,6 @@ export const githubDocsFrontmatterConfig = { 'partial-markdown-files': false, 'yml-files': false, }, - 'frontmatter-video-transcripts': { - // GHD011 - severity: 'error', - 'partial-markdown-files': false, - 'yml-files': false, - }, 'frontmatter-schema': { // GHD012 severity: 'error', diff --git a/src/content-linter/tests/fixtures/actions/index.md b/src/content-linter/tests/fixtures/actions/index.md index 5c907d336ce2..ea4f0a6e1ffd 100644 --- a/src/content-linter/tests/fixtures/actions/index.md +++ b/src/content-linter/tests/fixtures/actions/index.md @@ -1,8 +1,6 @@ --- title: GitHub Actions Documentation layout: product-landing -product_video: 'https://www.yourube.com/abc123' -product_video_transcript: '/video-transcripts/transcript--my-awesome-video' children: - /hidden - /not-hidden diff --git a/src/content-linter/tests/fixtures/early-access/index.md b/src/content-linter/tests/fixtures/early-access/index.md index adcb6a5e66b4..03e5e5488758 100644 --- a/src/content-linter/tests/fixtures/early-access/index.md +++ b/src/content-linter/tests/fixtures/early-access/index.md @@ -1,7 +1,6 @@ --- title: Early Access Documentation layout: product-landing -product_video_transcript: '/video-transcripts/transcript--my-awesome-video' children: - /hidden - /not-hidden diff --git a/src/content-linter/tests/fixtures/video-transcripts/index.md b/src/content-linter/tests/fixtures/video-transcripts/index.md deleted file mode 100644 index 61b3e14787b2..000000000000 --- a/src/content-linter/tests/fixtures/video-transcripts/index.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Video transcripts -hidden: true -intro: 'Find audio and visual transcripts for videos linked from this site.' -versions: - fpt: '*' - ghec: '*' - ghes: '*' -children: - - transcript-codespaces-your-instant-dev-box-in-the-cloud - - transcript-using-projects-for-feature-planning ---- diff --git a/src/content-linter/tests/fixtures/video-transcripts/transcript-codespaces-your-instant-dev-box-in-the-cloud.md b/src/content-linter/tests/fixtures/video-transcripts/transcript-codespaces-your-instant-dev-box-in-the-cloud.md deleted file mode 100644 index 528d0d95e9b2..000000000000 --- a/src/content-linter/tests/fixtures/video-transcripts/transcript-codespaces-your-instant-dev-box-in-the-cloud.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Transcript - "Codespaces - Your instant dev box in the cloud" -product_video: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc' ---- - -Some transcript text... diff --git a/src/content-linter/tests/fixtures/video-transcripts/transcript-using-projects-for-feature-planning.md b/src/content-linter/tests/fixtures/video-transcripts/transcript-using-projects-for-feature-planning.md deleted file mode 100644 index 8fbcb342170d..000000000000 --- a/src/content-linter/tests/fixtures/video-transcripts/transcript-using-projects-for-feature-planning.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Using Projects for feature planning ---- - -Some video transcript text... diff --git a/src/content-linter/tests/learning-track-liquid.ts b/src/content-linter/tests/learning-track-liquid.ts deleted file mode 100644 index cbb7df025ae9..000000000000 --- a/src/content-linter/tests/learning-track-liquid.ts +++ /dev/null @@ -1,43 +0,0 @@ -import yaml from 'js-yaml' -import { readFile } from 'fs/promises' - -import walk from 'walk-sync' -import { beforeAll, describe, expect, test } from 'vitest' - -import { liquid } from '@/content-render/index' - -const learningTrackRootPath = 'data/learning-tracks' -const yamlWalkOptions = { - globs: ['**/*.yml'], - directories: false, - includeBasePath: true, -} -const yamlFileList = walk(learningTrackRootPath, yamlWalkOptions).sort() - -describe('lint learning tracks', () => { - if (yamlFileList.length < 1) return - - describe.each(yamlFileList)('%s', (yamlAbsPath) => { - // Using any type because YAML content structure is dynamic and varies per file - let yamlContent: any - - beforeAll(async () => { - const fileContents = await readFile(yamlAbsPath, 'utf8') - yamlContent = await yaml.load(fileContents) - }) - - test('contains valid liquid', () => { - // Using any[] for toLint since it contains mixed string content from various YAML properties - const toLint: any[] = [] - // Using any for destructured params as YAML structure varies across different learning track files - for (const { title, description } of Object.values(yamlContent) as any[]) { - toLint.push(title) - toLint.push(description) - } - - for (const element of toLint) { - expect(() => liquid.parse(element), `${element} contains invalid liquid`).not.toThrow() - } - }) - }) -}) diff --git a/src/content-linter/tests/lint-frontmatter-links.ts b/src/content-linter/tests/lint-frontmatter-links.ts index 1b5fa16c7a9d..07b4b1e352af 100644 --- a/src/content-linter/tests/lint-frontmatter-links.ts +++ b/src/content-linter/tests/lint-frontmatter-links.ts @@ -41,34 +41,6 @@ describe('front matter', () => { return customErrorMessage } - // Test content with .includeGuides front matter - - const pagesWithIncludeGuides = pageList.filter((page) => page.includeGuides) - test.each(pagesWithIncludeGuides)( - '$relativePath .includeGuides have pristine links', - async (page) => { - const redirectsContext = { redirects, pages } - - const trouble = page - .includeGuides! // Using any type for uri because includeGuides can contain various URI formats - .map((uri: any, i: number) => checkURL(uri, i, redirectsContext)) - .filter(Boolean) - - const customErrorMessage = makeCustomErrorMessage(page, trouble, 'includeGuides') - expect(trouble.length, customErrorMessage).toEqual(0) - - const counts = new Map() - for (const guide of page.includeGuides!) { - counts.set(guide, (counts.get(guide) || 0) + 1) - } - const countUnique = counts.size - let notDistinctMessage = `In ${page.relativePath} there are duplicate links in .includeGuides` - const dupes = [...counts.entries()].filter(([, count]) => count > 1).map(([entry]) => entry) - notDistinctMessage += `\nTo fix this, remove: ${dupes.join(' and ')}` - expect(page.includeGuides!.length, notDistinctMessage).toEqual(countUnique) - }, - ) - // Test content with .featuredLinks front matter const pagesWithFeaturedLinks = pageList.filter((page) => page.featuredLinks) diff --git a/src/content-linter/tests/unit/frontmatter-video-transcripts.ts b/src/content-linter/tests/unit/frontmatter-video-transcripts.ts deleted file mode 100644 index 914730a3d335..000000000000 --- a/src/content-linter/tests/unit/frontmatter-video-transcripts.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { runRule } from '../../lib/init-test' -import { frontmatterVideoTranscripts } from '../../lib/linting-rules/frontmatter-video-transcripts' - -const GOOD_FIXTURE_LANDING = 'src/content-linter/tests/fixtures/actions/index.md' -const BAD_FIXTURE_LANDING = 'src/content-linter/tests/fixtures/early-access/index.md' -const GOOD_FIXTURE_TRANSCRIPT = - 'src/content-linter/tests/fixtures/video-transcripts/transcript-codespaces-your-instant-dev-box-in-the-cloud.md' -const BAD_FIXTURE_TRANSCRIPT = - 'src/content-linter/tests/fixtures/video-transcripts/transcript-using-projects-for-feature-planning.md' -const INDEX_VIDEO_TRANSCRIPT = 'src/content-linter/tests/fixtures/video-transcripts/index.md' -const fmOptions = { markdownlintOptions: { frontMatter: null } } - -describe(frontmatterVideoTranscripts.names.join(' - '), () => { - test('a video transcript with correct title and fm passes', async () => { - const result = await runRule(frontmatterVideoTranscripts, { - files: [GOOD_FIXTURE_TRANSCRIPT], - ...fmOptions, - }) - const errors = result[GOOD_FIXTURE_TRANSCRIPT] - expect(errors.length).toBe(0) - }) - test('a video transcript with bad title and fm fails', async () => { - const result = await runRule(frontmatterVideoTranscripts, { - files: [BAD_FIXTURE_TRANSCRIPT], - ...fmOptions, - }) - const errors = result[BAD_FIXTURE_TRANSCRIPT] - expect(errors.length).toBe(2) - }) - test('a product landing page with correct video transcript fm passes', async () => { - const result = await runRule(frontmatterVideoTranscripts, { - files: [GOOD_FIXTURE_LANDING], - ...fmOptions, - }) - const errors = result[GOOD_FIXTURE_LANDING] - expect(errors.length).toBe(0) - }) - test('a product landing page without required video transcript fm fails', async () => { - const result = await runRule(frontmatterVideoTranscripts, { - files: [BAD_FIXTURE_LANDING], - ...fmOptions, - }) - const errors = result[BAD_FIXTURE_LANDING] - expect(errors.length).toBe(1) - }) - test('a video transcript landing page is not checked for title and product_video properties', async () => { - const result = await runRule(frontmatterVideoTranscripts, { - files: [INDEX_VIDEO_TRANSCRIPT], - ...fmOptions, - }) - const errors = result[INDEX_VIDEO_TRANSCRIPT] - expect(errors.length).toBe(0) - }) -}) diff --git a/src/content-pipelines/state/copilot-cli.sha b/src/content-pipelines/state/copilot-cli.sha index 741b784371d4..88fc117fd962 100644 --- a/src/content-pipelines/state/copilot-cli.sha +++ b/src/content-pipelines/state/copilot-cli.sha @@ -1 +1 @@ -220f40fa27593ccceb482a0c5546fc151e3976e8 +e566c239b96a15dd2d7e2e32b67668227e21f417 diff --git a/src/content-render/scripts/move-content.ts b/src/content-render/scripts/move-content.ts index bdef7859e688..1624d6c7ae2c 100755 --- a/src/content-render/scripts/move-content.ts +++ b/src/content-render/scripts/move-content.ts @@ -28,7 +28,6 @@ import { execFileSync } from 'child_process' import { program } from 'commander' import chalk from 'chalk' import walk from 'walk-sync' -import yaml from 'js-yaml' import escapeStringRegexp from 'escape-string-regexp' import fm from '@/frame/lib/frontmatter' @@ -51,7 +50,6 @@ interface PositionInfo { // This is so you can optionally run it again the test fixtures root. const ROOT = process.env.ROOT || '.' const CONTENT_ROOT = path.resolve(path.join(ROOT, 'content')) -const DATA_ROOT = path.resolve(path.join(ROOT, 'data')) const REDIRECT_FROM_KEY = 'redirect_from' const CHILDREN_KEY = 'children' @@ -501,7 +499,7 @@ function editFiles(files: FileTuple[], updateParent: boolean, opts: MoveOptions) // frontmatter key. // See comment in the first loop above for why we're looping over the files // two times. - for (const [oldPath, newPath, oldHref, newHref] of files) { + for (const [oldPath, newPath, oldHref] of files) { const fileContent = fs.readFileSync(newPath, 'utf-8') const { content, data } = readFrontmatter(fileContent) if (!data) continue @@ -521,14 +519,6 @@ function editFiles(files: FileTuple[], updateParent: boolean, opts: MoveOptions) if (updateParent) { addToChildren(newPath, removeFromChildren(oldPath, opts), opts) } - - // Perhaps this was mentioned in a 'guide' in a learning track - for (const filePath of findInLearningTracks(oldHref)) { - changeLearningTracks(filePath, oldHref, newHref) - if (verbose) { - console.log(`Updated learning tracks in ${filePath}`) - } - } } // Add contentType frontmatter to moved files @@ -563,7 +553,7 @@ function undoFiles(files: FileTuple[], updateParent: boolean, opts: MoveOptions) const { verbose, git: useGit } = opts // First undo any edits to the file - for (const [oldPath, newPath, oldHref, newHref] of files) { + for (const [oldPath, newPath, oldHref] of files) { const fileContent = fs.readFileSync(newPath, 'utf-8') const { content, data } = readFrontmatter(fileContent) if (!data) continue @@ -583,14 +573,6 @@ function undoFiles(files: FileTuple[], updateParent: boolean, opts: MoveOptions) if (updateParent) { addToChildren(newPath, removeFromChildren(oldPath, opts), opts) } - - // Perhaps this was mentioned in a 'guide' in a learning track - for (const filePath of findInLearningTracks(newHref)) { - changeLearningTracks(filePath, newHref, oldHref) - if (verbose) { - console.log(`Updated learning tracks in ${filePath}`) - } - } } if (useGit) { const cmd = ['commit', '-a', '-m', `unset ${REDIRECT_FROM_KEY} on ${files.length} files`] @@ -601,40 +583,6 @@ function undoFiles(files: FileTuple[], updateParent: boolean, opts: MoveOptions) } } -function findInLearningTracks(href: string) { - const allFiles: string[] = walk(path.join(DATA_ROOT, 'learning-tracks'), { - globs: ['*.yml'], - includeBasePath: true, - directories: false, - }) - const found: string[] = [] - for (const filePath of allFiles) { - const tracks = yaml.load(fs.readFileSync(filePath, 'utf-8')) as Record< - string, - { guides?: string[] } - > - - if ( - Object.values(tracks).find((track) => { - const guides = track.guides || [] - return guides.includes(href) - }) - ) { - found.push(filePath) - } - } - return found -} - -function changeLearningTracks(filePath: string, oldHref: string, newHref: string) { - // Can't deserialize and serialize the Yaml because it would lose - // formatting and comments. So regex replace it. - const regex = new RegExp(`- ${oldHref}$`, 'gm') - const oldContent = fs.readFileSync(filePath, 'utf-8') - const newContent = oldContent.replace(regex, `- ${newHref}`) - fs.writeFileSync(filePath, newContent, 'utf-8') -} - function changeHomepageLinks(oldHref: string, newHref: string, verbose: boolean) { // Can't deserialize and serialize the Yaml because it would lose // formatting and comments. So regex replace it. diff --git a/src/data-directory/README.md b/src/data-directory/README.md index 77fc9dd37848..2ce8e3cf6792 100644 --- a/src/data-directory/README.md +++ b/src/data-directory/README.md @@ -3,18 +3,18 @@ Purpose-built utilities, schemas, and workflows that power our Liquid `{% data %}` and `{% indented_data_reference %}` tags, reusable content, UI strings, and feature metadata. This subject focuses on how we read, validate, and serve files in `data/` across languages. ## Purpose & scope -- Provide a consistent API (`getDataByLanguage`, `getDeepDataByLanguage`) to load `data/` files for Liquid rendering and server contexts. -- Enforce schemas for critical data (features, variables, learning tracks, release notes, tables, glossaries, code languages, CTAs). -- Ship CLI and CI helpers that keep `data/` clean (orphaned feature detection, deleted-feature PR guardrails). -- Exclude: content authoring guidance (see `content/`), page routing (see `src/app`/`src/frame`), and general linter rules (see `src/content-linter`). +* Provide a consistent API (`getDataByLanguage`, `getDeepDataByLanguage`) to load `data/` files for Liquid rendering and server contexts. +* Enforce schemas for critical data (features, variables, release notes, tables, glossaries, code languages, CTAs). +* Ship CLI and CI helpers that keep `data/` clean (orphaned feature detection, deleted-feature PR guardrails). +* Exclude: content authoring guidance (see `content/`), page routing (see `src/app`/`src/frame`), and general linter rules (see `src/content-linter`). ## Architecture & key assets -- `lib/get-data.ts`: translation-aware loader with memoized reads, forced-English exceptions, and UI data merging; used by Liquid tags and server contexts. -- `lib/data-directory.ts` + `lib/filename-to-key.ts`: generic walker that turns files into dotted-key objects with optional preprocessing. -- `lib/data-schemas/`: AJV schema registry that auto-discovers `data/tables/*.yml` schemas and registers other critical shapes (features, variables, learning tracks, release notes, glossaries, code languages, CTAs). -- Middleware: `middleware/data-tables.ts` caches table data into `req.context.tables` (English). -- Scripts: `scripts/find-orphaned-features/*` (detect/delete unused `data/features/*.yml`) and `scripts/deleted-features-pr-comment.ts` (warn on feature deletions in PRs). -- Tests: `tests/` cover schema validation, data loading, key normalization, and orphan detection fixtures. +* `lib/get-data.ts`: translation-aware loader with memoized reads, forced-English exceptions, and UI data merging; used by Liquid tags and server contexts. +* `lib/data-directory.ts` + `lib/filename-to-key.ts`: generic walker that turns files into dotted-key objects with optional preprocessing. +* `lib/data-schemas/`: AJV schema registry that auto-discovers `data/tables/*.yml` schemas and registers other critical shapes (features, variables, release notes, glossaries, code languages, CTAs). +* Middleware: `middleware/data-tables.ts` caches table data into `req.context.tables` (English). +* Scripts: `scripts/find-orphaned-features/*` (detect/delete unused `data/features/*.yml`) and `scripts/deleted-features-pr-comment.ts` (warn on feature deletions in PRs). +* Tests: `tests/` cover schema validation, data loading, key normalization, and orphan detection fixtures. ## Data loading contracts - `lib/get-data.ts` diff --git a/src/data-directory/lib/data-schemas/index.ts b/src/data-directory/lib/data-schemas/index.ts index 68753e47265d..262102a123f8 100644 --- a/src/data-directory/lib/data-schemas/index.ts +++ b/src/data-directory/lib/data-schemas/index.ts @@ -50,7 +50,6 @@ function loadTableSchemas(): DataSchemas { const manualSchemas: DataSchemas = { 'data/features': resolveSchemaPath('features.ts'), 'data/variables': resolveSchemaPath('variables.ts'), - 'data/learning-tracks': resolveSchemaPath('learning-tracks.ts'), 'data/release-notes': resolveSchemaPath('release-notes.ts'), 'data/code-languages.yml': resolveSchemaPath('code-languages.ts'), 'data/glossaries/candidates.yml': resolveSchemaPath('glossaries-candidates.ts'), diff --git a/src/data-directory/lib/data-schemas/learning-tracks.ts b/src/data-directory/lib/data-schemas/learning-tracks.ts deleted file mode 100644 index dcab4841cc74..000000000000 --- a/src/data-directory/lib/data-schemas/learning-tracks.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { schema } from '@/frame/lib/frontmatter' - -// Some learning tracks have `versions` blocks that match `versions` frontmatter, -// so we can import that part of the FM schema. -const versionsProps = Object.assign({}, schema.properties.versions) - -// `versions` are not required in learning tracks the way they are in FM. -delete versionsProps.required - -export default { - type: 'object', - additionalProperties: false, - patternProperties: { - '^[a-zA-Z-_]+$': { - type: 'object', - required: ['title', 'description', 'guides'], - additionalProperties: false, - properties: { - title: { - type: 'string', - lintable: true, - }, - description: { - type: 'string', - lintable: true, - }, - guides: { - type: 'array', - items: { - type: 'string', - // matches Liquid tags and URLs with trailing backslash - // if this regex becomes problematic, we can remove it - pattern: '^(\\{%.*%\\})?\\s*(\\/((\\w|-|\\.))+)+\\s*(\\{%.*%\\})?$', - }, - }, - versions: versionsProps, - }, - }, - }, -} diff --git a/src/data-directory/lib/get-data.ts b/src/data-directory/lib/get-data.ts index fffa242f63d2..ba5131e2dc02 100644 --- a/src/data-directory/lib/get-data.ts +++ b/src/data-directory/lib/get-data.ts @@ -247,15 +247,6 @@ function getDataByDir( return getYamlContent(dir, fullPath.join(path.sep), englishRoot) } - if (first === 'learning-tracks') { - const key = split.pop()! - const basename = split.pop()! - fullPath.push(...split) - fullPath.push(`${basename}.yml`) - const allData = getYamlContent(dir, fullPath.join(path.sep), englishRoot) - return key ? allData[key] : undefined - } - throw new Error(`Can't find the key '${dottedPath}' in the scope.`) } diff --git a/src/fixtures/fixtures/article-with-videos.md b/src/fixtures/fixtures/article-with-videos.md deleted file mode 100644 index 293a5ff3f33a..000000000000 --- a/src/fixtures/fixtures/article-with-videos.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Article with featuredLinks -versions: - free-pro-team: '*' -featuredLinks: - videos: - - title: codespaces - href: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc' - - title: more codespaces - href: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc' - - title: even more codespaces - href: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc' - videosHeading: Custom Videos heading -layout: product-landing ---- diff --git a/src/fixtures/fixtures/content/code-security/guides.md b/src/fixtures/fixtures/content/code-security/guides.md deleted file mode 100644 index 824e63aa871c..000000000000 --- a/src/fixtures/fixtures/content/code-security/guides.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Guides for cool security -intro: This page has a deliberately whimsical `title` for testing purposes. -layout: product-guides -versions: - fpt: '*' - ghes: '*' - ghec: '*' -includeGuides: - - /code-security/getting-started/quickstart - - /code-security/getting-started/securing-your-organization -learningTracks: - - foo_bar - - bar_foo ---- diff --git a/src/fixtures/fixtures/content/code-security/index.md b/src/fixtures/fixtures/content/code-security/index.md index c01870c61ff9..3255d78d76c7 100644 --- a/src/fixtures/fixtures/content/code-security/index.md +++ b/src/fixtures/fixtures/content/code-security/index.md @@ -13,7 +13,6 @@ versions: ghec: '*' children: - /getting-started - - /guides - /secret-scanning - /codeql-cli --- diff --git a/src/fixtures/fixtures/content/codespaces/guides.md b/src/fixtures/fixtures/content/codespaces/guides.md deleted file mode 100644 index 8e11ac0bc1a0..000000000000 --- a/src/fixtures/fixtures/content/codespaces/guides.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Guides for GitHub Codespaces -intro: Learn how to make the most of GitHub Codespaces. -layout: product-guides -versions: - fpt: '*' -includeGuides: - - /get-started/start-your-journey/hello-world -learningTracks: - - foo_bar ---- diff --git a/src/fixtures/fixtures/content/codespaces/index.md b/src/fixtures/fixtures/content/codespaces/index.md index 024b1c2aba26..c3879f8d8957 100644 --- a/src/fixtures/fixtures/content/codespaces/index.md +++ b/src/fixtures/fixtures/content/codespaces/index.md @@ -3,6 +3,5 @@ title: GitHub Codespaces documentation intro: Develop in a codespace. versions: fpt: '*' -children: - - /guides +children: [] --- diff --git a/src/fixtures/fixtures/content/get-started/index.md b/src/fixtures/fixtures/content/get-started/index.md index 3dd5930c13cc..6dea171a7489 100644 --- a/src/fixtures/fixtures/content/get-started/index.md +++ b/src/fixtures/fixtures/content/get-started/index.md @@ -33,7 +33,6 @@ children: - /start-your-journey - /foo - /sidebar-test - - /video-transcripts - /minitocs - /liquid - /markdown @@ -51,6 +50,4 @@ children: communityRedirect: name: Provide HubGit Feedback href: 'https://hubgit.com/orgs/community/discussions/categories/get-started' -product_video: 'https://www.yourube.com/abc123' -product_video_transcript: '/video-transcripts/transcript--my-awesome-video' --- diff --git a/src/fixtures/fixtures/content/get-started/video-transcripts/index.md b/src/fixtures/fixtures/content/get-started/video-transcripts/index.md deleted file mode 100644 index ca560d142210..000000000000 --- a/src/fixtures/fixtures/content/get-started/video-transcripts/index.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Transcripts -hidden: true -versions: - fpt: '*' - ghes: '*' - ghec: '*' -children: - - /transcript--my-awesome-video ---- diff --git a/src/fixtures/fixtures/content/get-started/video-transcripts/transcript--my-awesome-video.md b/src/fixtures/fixtures/content/get-started/video-transcripts/transcript--my-awesome-video.md deleted file mode 100644 index b48cc73c359d..000000000000 --- a/src/fixtures/fixtures/content/get-started/video-transcripts/transcript--my-awesome-video.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Transcript - My awesome video -product_video: 'https://www.yourube.com/abc123' -versions: - fpt: '*' - ghes: '*' - ghec: '*' ---- - -This is a transcript diff --git a/src/fixtures/fixtures/content/index.md b/src/fixtures/fixtures/content/index.md index ba405bde1d67..c08cc1a82312 100644 --- a/src/fixtures/fixtures/content/index.md +++ b/src/fixtures/fixtures/content/index.md @@ -34,7 +34,6 @@ children: - rest - webhooks - graphql - - video-transcripts # - account-and-profile - authentication # - repositories diff --git a/src/fixtures/fixtures/content/video-transcripts/index.md b/src/fixtures/fixtures/content/video-transcripts/index.md deleted file mode 100644 index c9d340d39799..000000000000 --- a/src/fixtures/fixtures/content/video-transcripts/index.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Video transcripts -intro: 'Collection of video transcripts for accessibility and reference.' -versions: - fpt: '*' - ghes: '*' - ghec: '*' -children: - - /transcript--my-awesome-video ---- - -This section contains transcripts for videos used throughout the documentation. diff --git a/src/fixtures/fixtures/content/video-transcripts/transcript--my-awesome-video.md b/src/fixtures/fixtures/content/video-transcripts/transcript--my-awesome-video.md deleted file mode 100644 index b48cc73c359d..000000000000 --- a/src/fixtures/fixtures/content/video-transcripts/transcript--my-awesome-video.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Transcript - My awesome video -product_video: 'https://www.yourube.com/abc123' -versions: - fpt: '*' - ghes: '*' - ghec: '*' ---- - -This is a transcript diff --git a/src/fixtures/fixtures/data/learning-tracks/code-security.yml b/src/fixtures/fixtures/data/learning-tracks/code-security.yml deleted file mode 100644 index 40d6db9ae6d7..000000000000 --- a/src/fixtures/fixtures/data/learning-tracks/code-security.yml +++ /dev/null @@ -1,14 +0,0 @@ -foo_bar: - title: Fix the plumbing - description: A couple easy ones - guides: - - /code-security/getting-started/quickstart - - /code-security/getting-started/securing-your-organization - -bar_foo: - title: Plumb the fixing - description: The reverse of the one above - guides: - - /code-security/getting-started/securing-your-organization - - /code-security/getting-started/quickstart - - /rest/actions diff --git a/src/fixtures/fixtures/data/learning-tracks/codespaces.yml b/src/fixtures/fixtures/data/learning-tracks/codespaces.yml deleted file mode 100644 index 78343b79c7ba..000000000000 --- a/src/fixtures/fixtures/data/learning-tracks/codespaces.yml +++ /dev/null @@ -1,6 +0,0 @@ -# Codespaces learning tracks -foo_bar: - title: Sample learning track - description: A sample track for testing - guides: - - /get-started/start-your-journey/hello-world diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml index a9412e255088..a3c072b1cc46 100644 --- a/src/fixtures/fixtures/data/ui.yml +++ b/src/fixtures/fixtures/data/ui.yml @@ -90,7 +90,6 @@ toc: popular: Popular startHere: Start here whats_new: What's new - videos: Videos all_changelogs: All changelog posts pages: article_version: 'Article version' @@ -98,7 +97,6 @@ pages: all_enterprise_releases: All Enterprise Server releases about_versions: About versions permissions_callout_title: Who can use this feature? - video_from_transcript: See video for this transcript copy_as_markdown: Copy as Markdown copy_as_markdown_desc: Use with any LLM view_as_markdown: View as Markdown @@ -285,6 +283,7 @@ product_landing: try_ghas_for_free: Try GitHub Advanced Security for free generate_secret_risk_assessment_report_for_free: Find out how to run a free secret risk assessment plan_your_migration: Plan your migration + ado_key_differences: Key differences between Azure DevOps and GitHub releases: Releases guides: Guides explore_guides: Explore guides @@ -305,32 +304,8 @@ product_landing: docs: docs explore_release_notes: Explore release notes view: View all - view_transcript: View video transcript all_docs: 'All {{ title }} docs' all_content: 'View all {{ title }} content' -product_guides: - learning_paths_title: '{{ name }} learning paths' - start_path: Start learning path - learning_paths_desc: Learning paths are a collection of guides that help you master a particular subject. - more_guides: more guides - load_more: Load more guides - all_guides_title: 'All {{ name }} guides' - filter_instructions: Filter the guide list using these controls - guides_found: - multiple: '{n} guides found' - one: 1 guide found - none: No guides found - guide_types: - get-started: Quickstart - concepts: Concepts - how-tos: How-to guide - tutorials: Tutorial - reference: Reference -learning_track_nav: - prev_guide: Previous - next_guide: Next - more_guides: More guides → - current_progress: '{i} of {n} in learning path' journey_track_nav: prev_article: Previous next_article: Next diff --git a/src/fixtures/tests/guides.ts b/src/fixtures/tests/guides.ts deleted file mode 100644 index ff4adb7d231d..000000000000 --- a/src/fixtures/tests/guides.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { describe, expect, test } from 'vitest' -import type { CheerioAPI } from 'cheerio' - -import { get, getDOMCached as getDOM } from '@/tests/helpers/e2etest' - -describe('guides', () => { - test("page's title should be document title", async () => { - const $: CheerioAPI = await getDOM('/code-security/guides') - // This is what you'd find in src/fixtures/fixtures/content/code-security/guides.md - const title = 'Guides for cool security' - expect($('title').text()).toMatch(title) - expect($('h1').text()).toMatch(title) - const learningPaths = $('h2#learning-paths') - expect(learningPaths.text()).toMatch('HubGit Code security learning paths') - const allGuides = $('h2#all-guides') - expect(allGuides.text()).toMatch('All HubGit Code security guides') - }) -}) - -describe('learning tracks', () => { - test('start the first learning track and come back via the navigation banner', async () => { - const $: CheerioAPI = await getDOM('/code-security/guides') - const links = $('[data-testid=learning-track] a') - const link = links - .filter((_: number, el: any) => $(el).text() === 'Start learning path') - .first() - expect(link.attr('href')).toMatch('learn=foo_bar') - expect(link.attr('href')).toMatch('learnProduct=code-security') - - // Go the first "Start learning path" link - const $2: CheerioAPI = await getDOM(link.attr('href')!) - const card2 = $2('[data-testid=learning-track-card]') - // The card has 2 links. One to go back to the guide page - // whose title is the name of the learning track - const backLink = card2.find('a').first() - expect(backLink.attr('href')).toBe('/en/code-security/guides') - expect(backLink.text()).toBe('Fix the plumbing') - // Underneath it says "1 of in learning path" - const span = card2.find('span').filter((_: number, el: any) => $(el).text().includes('1 of 2')) - expect(span.text()).toBe('1 of 2 in learning path') - const nextWrapper = card2 - .find('span') - .filter((_: number, el: any) => $(el).text().includes('Next')) - expect(nextWrapper.length).toBe(1) - const nextLink = nextWrapper.find('a').first() - expect(nextLink.attr('href')).toMatch( - 'code-security/getting-started/securing-your-organization', - ) - expect(nextLink.attr('href')).toMatch('learn=foo_bar') - expect(nextLink.attr('href')).toMatch('learnProduct=code-security') - expect(nextLink.text()).toBe('Securing your organization') - // There's also a nav at the bottom of the page - const nav2 = $2('[data-testid=learning-track-nav]') - const navNextLink = nav2.find('a').first() - expect(navNextLink.attr('href')).toMatch('learn=foo_bar') - expect(navNextLink.attr('href')).toMatch('learnProduct=code-security') - expect(navNextLink.text()).toBe('Securing your organization') - - // Go to the next (last) page - const $3: CheerioAPI = await getDOM(nextLink.attr('href')!) - const card3 = $3('[data-testid=learning-track-card]') - const span3 = card3.find('span').filter((_: number, el: any) => $(el).text().includes('2 of 2')) - expect(span3.text()).toBe('2 of 2 in learning path') - // No "Next:" link now - const nextWrapper3 = card3 - .find('span') - .filter((_: number, el: any) => $(el).text().includes('Next')) - expect(nextWrapper3.length).toBe(0) - const moreGuidesLink = card3 - .find('a') - .filter((_: number, el: any) => $(el).text().includes('More guides')) - expect(moreGuidesLink.attr('href')).toBe('/en/code-security/guides') - - // On this last page, the nav will link to the previous page - const nav3 = $2('[data-testid=learning-track-nav]') - const navPrevLink = nav3.find('a').first() - expect(navPrevLink.attr('href')).toMatch( - 'code-security/getting-started/securing-your-organization', - ) - }) - - test('with and without a valid ?learn= query string', async () => { - // Valid - { - const $: CheerioAPI = await getDOM('/code-security/getting-started/quickstart?learn=foo_bar') - expect($('[data-testid=learning-track-card]').length).toBe(1) - expect($('[data-testid=learning-track-nav]').length).toBe(1) - } - // Invalid - { - const $: CheerioAPI = await getDOM( - '/code-security/getting-started/quickstart?learn=blablainvalid', - ) - expect($('[data-testid=learning-track-card]').length).toBe(0) - expect($('[data-testid=learning-track-nav]').length).toBe(0) - } - // Empty - { - const $: CheerioAPI = await getDOM('/code-security/getting-started/quickstart?learn=') - expect($('[data-testid=learning-track-card]').length).toBe(0) - expect($('[data-testid=learning-track-nav]').length).toBe(0) - } - // Missing - { - const $: CheerioAPI = await getDOM('/code-security/getting-started/quickstart') - expect($('[data-testid=learning-track-card]').length).toBe(0) - expect($('[data-testid=learning-track-nav]').length).toBe(0) - } - }) - - // Skipping this test due to flakes... does this page actually have a learning track on it? - test.skip('REST category learning track article works', async () => { - const response = await get('/rest/actions?learnProduct=code-security&learn=bar_foo', { - followAllRedirects: true, - }) - expect(response.statusCode).toBe(200) - }) -}) diff --git a/src/fixtures/tests/playwright-a11y.spec.ts b/src/fixtures/tests/playwright-a11y.spec.ts index 11af57145658..0f518aaab224 100644 --- a/src/fixtures/tests/playwright-a11y.spec.ts +++ b/src/fixtures/tests/playwright-a11y.spec.ts @@ -5,7 +5,6 @@ import { turnOffExperimentsInPage, turnOnExperimentsInPage } from '../helpers/tu const pages: { [key: string]: string } = { category: '/actions/category', codeAnnotations: '/get-started/markdown/code-annotations', - guides: '/code-security/guides', homepage: '/', learningPath: '/code-security/getting-started/quickstart?learn=foo_bar&learnProduct=code-security', @@ -18,8 +17,6 @@ const pages: { [key: string]: string } = { search: '/search?q=playwright', switchers: '/get-started/liquid/tool-platform-switcher', tableWithHeaders: '/get-started/liquid/table-row-headers', - video: '/get-started', - videoTranscript: '/get-started/video-transcripts/transcript--my-awesome-video', } // create a test for each page, will eventually be separated into finer grain tests diff --git a/src/fixtures/tests/video-transcripts.ts b/src/fixtures/tests/video-transcripts.ts deleted file mode 100644 index 36a4f6adca1c..000000000000 --- a/src/fixtures/tests/video-transcripts.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, test } from 'vitest' -import type { CheerioAPI } from 'cheerio' - -import { getDOM } from '@/tests/helpers/e2etest' - -describe('transcripts', () => { - describe('product landing page', () => { - test('video link from product landing page leads to video', async () => { - const $: CheerioAPI = await getDOM('/en/get-started') - expect($('a#product-video').attr('href')).toBe( - '/en/video-transcripts/transcript--my-awesome-video', - ) - }) - }) - - describe('transcript page', () => { - test('video link from transcript leads to video', async () => { - const $: CheerioAPI = await getDOM( - '/en/get-started/video-transcripts/transcript--my-awesome-video', - ) - expect($('a#product-video').attr('href')).toBe('https://www.yourube.com/abc123') - }) - }) -}) diff --git a/src/frame/README.md b/src/frame/README.md index 3ba5bce80ee6..4837c54fd181 100644 --- a/src/frame/README.md +++ b/src/frame/README.md @@ -102,7 +102,6 @@ Nearly every subject interacts with frame: - [`src/redirects`](../redirects/README.md) - URL redirect handling - [`src/content-render`](../content-render/README.md) - Markdown rendering - [`src/landings`](../landings/README.md) - Landing page layouts -- [`src/learning-track`](../learning-track/README.md) - Learning track navigation ### Ownership - Team: Docs Engineering diff --git a/src/frame/components/article/ArticlePage.tsx b/src/frame/components/article/ArticlePage.tsx index a0a39806b2b5..d15cde68d384 100644 --- a/src/frame/components/article/ArticlePage.tsx +++ b/src/frame/components/article/ArticlePage.tsx @@ -1,12 +1,10 @@ import { useRouter } from 'next/router' import dynamic from 'next/dynamic' import cx from 'classnames' -import { LinkExternalIcon } from '@primer/octicons-react' +import { useArticleContext } from '@/frame/components/context/ArticleContext' import { DefaultLayout } from '@/frame/components/DefaultLayout' import { ArticleTitle } from '@/frame/components/article/ArticleTitle' -import { useArticleContext } from '@/frame/components/context/ArticleContext' -import { LearningTrackNav } from '@/learning-track/components/article/LearningTrackNav' import { MarkdownContent } from '@/frame/components/ui/MarkdownContent' import { Lead } from '@/frame/components/ui/Lead' import { PermissionsStatement } from '@/frame/components/ui/PermissionsStatement' @@ -15,11 +13,8 @@ import { ArticleInlineLayout } from './ArticleInlineLayout' import { PlatformPicker } from '@/tools/components/PlatformPicker' import { ToolPicker } from '@/tools/components/ToolPicker' import { MiniTocs } from '@/frame/components/ui/MiniTocs' -import { LearningTrackCard } from '@/learning-track/components/article/LearningTrackCard' import { RestRedirect } from '@/rest/components/RestRedirect' import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs' -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' @@ -42,17 +37,13 @@ export const ArticlePage = () => { includesPlatformSpecificContent, includesToolSpecificContent, product, - productVideoUrl, miniTocItems, - currentLearningTrack, currentJourneyTrack, supportPortalVaIframeProps, currentLayout, currentPath, } = useArticleContext() - const isLearningPath = !!currentLearningTrack?.trackName const isJourneyTrack = !!currentJourneyTrack?.trackId - const { t } = useTranslation(['pages']) const introProp = ( <> @@ -78,7 +69,6 @@ export const ArticlePage = () => { const toc = ( <> - {isLearningPath && } {isJourneyTrack && } {miniTocItems.length > 1 && } @@ -86,15 +76,6 @@ export const ArticlePage = () => { const articleContents = (
- {productVideoUrl && ( -
- - - {t('video_from_transcript')} - -
- )} - {renderedPage} {effectiveDate && ( @@ -126,11 +107,6 @@ export const ArticlePage = () => { > {articleContents} - {isLearningPath ? ( -
- -
- ) : null} {isJourneyTrack ? (
@@ -157,11 +133,6 @@ export const ArticlePage = () => { {articleContents} - {isLearningPath ? ( -
- -
- ) : null} {isJourneyTrack ? (
diff --git a/src/frame/components/context/ArticleContext.tsx b/src/frame/components/context/ArticleContext.tsx index 22cc1cf223ad..a2cf29c77171 100644 --- a/src/frame/components/context/ArticleContext.tsx +++ b/src/frame/components/context/ArticleContext.tsx @@ -3,16 +3,6 @@ import { createContext, useContext } from 'react' import type { JSX } from 'react' import type { JourneyContext } from '@/journeys/lib/journey-path-resolver' -export type LearningTrack = { - trackTitle: string - trackName: string - trackProduct: string - prevGuide?: { href: string; title: string } - nextGuide?: { href: string; title: string } - numberOfGuides: number - currentGuideIndex: number -} - export type MiniTocItem = { platform?: string contents: { @@ -34,8 +24,6 @@ export type ArticleContextT = { defaultPlatform?: string defaultTool?: string product?: string - productVideoUrl?: string - currentLearningTrack?: LearningTrack currentJourneyTrack?: JourneyContext detectedPlatforms: Array detectedTools: Array @@ -100,8 +88,6 @@ export const getArticleContextFromRequest = (req: any): ArticleContextT => { defaultPlatform: page.defaultPlatform || '', defaultTool: page.defaultTool || '', product: page.product || '', - productVideoUrl: page.product_video || '', - currentLearningTrack: req.context.currentLearningTrack, currentJourneyTrack: req.context.currentJourneyTrack, detectedPlatforms: page.detectedPlatforms || [], detectedTools: page.detectedTools || [], diff --git a/src/frame/components/context/CategoryLandingContext.tsx b/src/frame/components/context/CategoryLandingContext.tsx index cb5acc207abd..30837f4eb39b 100644 --- a/src/frame/components/context/CategoryLandingContext.tsx +++ b/src/frame/components/context/CategoryLandingContext.tsx @@ -1,5 +1,4 @@ import { createContext, useContext } from 'react' -import { LearningTrack } from './ArticleContext' import { FeaturedLink, getFeaturedLinksFromReq } from '@/landings/components/ProductLandingContext' import type { TocItem } from '@/landings/types' import { mapRawTocItemToTocItem } from '@/landings/types' @@ -14,7 +13,6 @@ export type CategoryLandingContextT = { variant?: 'compact' | 'expanded' featuredLinks: Record> renderedPage: string - currentLearningTrack?: LearningTrack currentLayout: string spotlight?: SpotlightItem[] } @@ -45,7 +43,6 @@ export const getCategoryLandingContextFromRequest = (req: any): CategoryLandingC variant: req.context.genericTocFlat ? 'expanded' : 'compact', featuredLinks: getFeaturedLinksFromReq(req), renderedPage: req.context.renderedPage, - currentLearningTrack: req.context.currentLearningTrack, currentLayout: req.context.currentLayoutName, spotlight: req.context.page.spotlight, } diff --git a/src/frame/components/context/TocLandingContext.tsx b/src/frame/components/context/TocLandingContext.tsx index 4c536af4fa25..e83ee2e22a5a 100644 --- a/src/frame/components/context/TocLandingContext.tsx +++ b/src/frame/components/context/TocLandingContext.tsx @@ -1,5 +1,4 @@ import { createContext, useContext } from 'react' -import { LearningTrack } from './ArticleContext' import { FeaturedLink, getFeaturedLinksFromReq } from '@/landings/components/ProductLandingContext' import type { SimpleTocItem } from '@/landings/types' import { mapRawTocItemToSimpleTocItem } from '@/landings/types' @@ -13,7 +12,6 @@ export type TocLandingContextT = { variant?: 'compact' | 'expanded' featuredLinks: Record> renderedPage: string - currentLearningTrack?: LearningTrack } export const TocLandingContext = createContext(null) @@ -40,6 +38,5 @@ export const getTocLandingContextFromRequest = (req: any): TocLandingContextT => variant: req.context.genericTocFlat ? 'expanded' : 'compact', featuredLinks: getFeaturedLinksFromReq(req), renderedPage: req.context.renderedPage, - currentLearningTrack: req.context.currentLearningTrack, } } diff --git a/src/frame/lib/frontmatter.ts b/src/frame/lib/frontmatter.ts index 5ef5b4af6ddb..3f1ec1339fca 100644 --- a/src/frame/lib/frontmatter.ts +++ b/src/frame/lib/frontmatter.ts @@ -37,7 +37,6 @@ const layoutNames = [ 'default', 'graphql-explorer', 'product-landing', - 'product-guides', 'release-notes', 'inline', 'category-landing', @@ -163,24 +162,6 @@ export const schema: Schema = { type: 'string', translatable: true, }, - videos: { - type: 'array', - items: { - type: 'object', - properties: { - title: { - type: 'string', - }, - href: { - type: 'string', - }, - }, - }, - }, - // allows you to use an alternate heading for the videos column - videosHeading: { - type: 'string', - }, }, }, // Shown in `product-landing.html` "What's new" section @@ -202,11 +183,11 @@ export const schema: Schema = { type: 'string', enum: contentTypesEnum, }, - includeGuides: { - type: 'array', - }, - learningTracks: { - type: 'array', + // Optional heading override for the single-track journey landing UI + journeyArticlesHeading: { + type: 'string', + translatable: true, + description: 'Override the default "Articles" heading on single-track journey landing pages', }, // Journey tracks for journey landing pages journeyTracks: { @@ -258,14 +239,6 @@ export const schema: Schema = { beta_product: { type: 'boolean', }, - // Show in `product-landing.html` - product_video: { - type: 'string', - }, - // Show in `product-landing.html` - product_video_transcript: { - type: 'string', - }, // Hero image for landing pages heroImage: { type: 'string', diff --git a/src/learning-track/lib/get-link-data.ts b/src/frame/lib/get-link-data.ts similarity index 87% rename from src/learning-track/lib/get-link-data.ts rename to src/frame/lib/get-link-data.ts index 236c138b3589..00920797e9d5 100644 --- a/src/learning-track/lib/get-link-data.ts +++ b/src/frame/lib/get-link-data.ts @@ -4,7 +4,21 @@ import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-v import removeFPTFromPath from '@/versions/lib/remove-fpt-from-path' import { renderContent } from '@/content-render/index' import { executeWithFallback } from '@/languages/lib/render-with-fallback' -import { Context, LinkOptions, ProcessedLink } from './types' +import type { Context, Page } from '@/types' + +export interface LinkOptions { + title?: boolean + intro?: boolean + fullTitle?: boolean +} + +export interface ProcessedLink { + href: string + page: Page + title?: string + fullTitle?: string + intro?: string +} // rawLinks is an array of paths: [ '/foo' ] // we need to convert it to an array of localized objects: [ { href: '/en/foo', title: 'Foo', intro: 'Description here' } ] @@ -41,20 +55,19 @@ export default async function getLinkData( } async function processLink( - link: string | { href: string }, + link: string, context: Context, options: LinkOptions, ): Promise { const opts: { textOnly: boolean; preferShort?: boolean } = { textOnly: true } - const linkHref = typeof link === 'string' ? link : link.href // Parse the link in case it includes Liquid conditionals - const linkPath = linkHref.includes('{') + const linkPath = link.includes('{') ? await executeWithFallback( context, - () => renderContent(linkHref, context, opts), - (enContext: Context) => renderContent(linkHref, enContext, opts), + () => renderContent(link, context, opts), + (enContext: Context) => renderContent(link, enContext, opts), ) - : linkHref + : link // If the link was `{% ifversion ghes %}/admin/foo/bar{% endifversion %}` // the `context.currentVersion` was `enterprise-cloud`, the final // output would become '' (empty string). diff --git a/src/frame/lib/page.ts b/src/frame/lib/page.ts index eae53f6b6343..f690eadb5468 100644 --- a/src/frame/lib/page.ts +++ b/src/frame/lib/page.ts @@ -8,11 +8,11 @@ import getEnglishHeadings from '@/languages/lib/get-english-headings' import { getAlertTitles } from '@/languages/lib/get-alert-titles' import Permalink from './permalink' import { renderContent } from '@/content-render/index' -import processLearningTracks from '@/learning-track/lib/process-learning-tracks' + import { productMap } from '@/products/lib/all-products' import slash from 'slash' import readFileContents from './read-file-contents' -import getLinkData from '@/learning-track/lib/get-link-data' + import getDocumentType from '@/events/lib/get-document-type' import { allTools } from '@/tools/lib/all-tools' import { renderContentWithFallback } from '@/languages/lib/render-with-fallback' @@ -58,12 +58,6 @@ type CommunityRedirect = { href: string } -type GuideWithContentType = { - href: string - title: string - contentType?: string -} - export class FrontmatterErrorsError extends Error { public frontmatterErrors: string[] @@ -89,10 +83,6 @@ class Page { public showMiniToc?: boolean public hidden?: boolean public redirect_from?: string[] - public learningTracks?: any[] - public rawLearningTracks?: string[] - public includeGuides?: GuideWithContentType[] - public rawIncludeGuides?: string[] public introLinks?: Record public rawIntroLinks?: Record public carousels?: Record @@ -221,8 +211,6 @@ class Page { this.rawShortTitle = this.shortTitle this.rawProduct = this.product this.rawPermissions = this.permissions - this.rawLearningTracks = this.learningTracks - this.rawIncludeGuides = this.includeGuides as any this.rawIntroLinks = this.introLinks this.rawCarousels = this.carousels @@ -359,12 +347,6 @@ class Page { this.permissions = await renderContentWithFallback(this, 'rawPermissions', context) } - // Learning tracks may contain Liquid and need to have versioning processed. - if (this.rawLearningTracks) { - const { learningTracks } = await processLearningTracks(this.rawLearningTracks, context) - this.learningTracks = learningTracks - } - // introLinks may contain Liquid and need to have versioning processed. if (this.rawIntroLinks) { const introLinks: Record = {} @@ -377,19 +359,6 @@ class Page { this.introLinks = introLinks } - if (this.rawIncludeGuides) { - this.includeGuides = (await getLinkData( - this.rawIncludeGuides, - context, - )) as GuideWithContentType[] - this.includeGuides?.map((guide: any) => { - const { page } = guide - guide.contentType = page.contentType - delete guide.page - return guide - }) - } - // set a flag so layout knows whether to render a mac/windows/linux switcher element // Remember, the values of platform is matched in // the handleInvalidQuerystringValues shielding middleware. diff --git a/src/frame/middleware/helmet.ts b/src/frame/middleware/helmet.ts index 302bd0688adf..c5c083ee9a79 100644 --- a/src/frame/middleware/helmet.ts +++ b/src/frame/middleware/helmet.ts @@ -15,7 +15,7 @@ const GITHUB_DOMAINS = [ const DEFAULT_OPTIONS = { crossOriginResourcePolicy: true, - crossOriginEmbedderPolicy: false, // doesn't work with youtube + crossOriginEmbedderPolicy: false, referrerPolicy: { policy: 'no-referrer-when-downgrade' as const, }, @@ -48,7 +48,6 @@ const DEFAULT_OPTIONS = { ? 'https://support.github.com' : // Assume that a developer is not testing the VA iframe locally if this env var is not set process.env.SUPPORT_PORTAL_URL || '', - 'https://www.youtube-nocookie.com', ].filter(Boolean) as string[], frameAncestors: isDev ? ['*'] : [...GITHUB_DOMAINS], styleSrc: [...GITHUB_DOMAINS, "'self'", "'unsafe-inline'", 'data:'], diff --git a/src/frame/middleware/index.ts b/src/frame/middleware/index.ts index 1a19c09751e2..5383edbd4604 100644 --- a/src/frame/middleware/index.ts +++ b/src/frame/middleware/index.ts @@ -52,7 +52,6 @@ import features from '@/versions/middleware/features' import productExamples from './context/product-examples' import productGroups from './context/product-groups' import featuredLinks from '@/landings/middleware/featured-links' -import learningTrack from '@/learning-track/middleware/learning-track' import journeyTrack from '@/journeys/middleware/journey-track' import next from './next' import renderPage from './render-page' @@ -285,7 +284,6 @@ export default function index(app: Express) { app.use(asyncMiddleware(generalSearchMiddleware)) app.use(asyncMiddleware(featuredLinks)) app.use(asyncMiddleware(resolveCarousels)) - app.use(asyncMiddleware(learningTrack)) app.use(asyncMiddleware(journeyTrack)) if (ENABLE_FASTLY_TESTING) { diff --git a/src/frame/tests/page.ts b/src/frame/tests/page.ts index 591c0d8fe247..5d757c5e3b87 100644 --- a/src/frame/tests/page.ts +++ b/src/frame/tests/page.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from 'url' import path from 'path' import { load } from 'cheerio' -import { beforeAll, beforeEach, describe, expect, test } from 'vitest' +import { beforeAll, describe, expect, test } from 'vitest' import Page, { FrontmatterErrorsError } from '@/frame/lib/page' import { allVersions } from '@/versions/lib/all-versions' @@ -263,37 +263,6 @@ describe('Page class', () => { }) }) - describe('videos', () => { - let page: Page | undefined - - beforeEach(async () => { - page = await Page.init({ - relativePath: 'article-with-videos.md', - basePath: path.join(__dirname, '../../../src/fixtures/fixtures'), - languageCode: 'en', - }) - }) - - test('includes videos specified in the featuredLinks frontmatter', async () => { - expect((page as any)!.featuredLinks.videos).toStrictEqual([ - { - title: 'codespaces', - href: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc', - }, - { - title: 'more codespaces', - href: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc', - }, - { - title: 'even more codespaces', - href: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc', - }, - ]) - - expect((page as any)!.featuredLinks.videosHeading).toBe('Custom Videos heading') - }) - }) - describe('introLinks', () => { test('includes the links specified in the introLinks frontmatter', async () => { const page = await Page.init({ @@ -464,7 +433,7 @@ describe('catches errors thrown in Page class', () => { }) describe('versioning optional attributes', () => { - test("re-rendering set appropriate 'product', 'permissions', 'learningTracks'", async () => { + test("re-rendering set appropriate 'product', 'permissions'", async () => { const page = await Page.init({ relativePath: 'page-with-optional-attributes.md', basePath: path.join(__dirname, '../../../src/fixtures/fixtures'), diff --git a/src/journeys/README.md b/src/journeys/README.md index 62b78f5bc72d..7b19ccd3d3d0 100644 --- a/src/journeys/README.md +++ b/src/journeys/README.md @@ -5,7 +5,7 @@ The **journeys** subject provides guided learning experiences (called "tracks") ## Purpose & Scope This subject is responsible for: -- Rendering journey landing pages that display multiple learning tracks +- Rendering journey landing pages that display multiple journey tracks - Providing prev/next navigation within journey track articles - Resolving journey context based on the current article path - Rendering Liquid templates in journey metadata (titles, descriptions, guide paths) diff --git a/src/journeys/lib/get-link-data.ts b/src/journeys/lib/get-link-data.ts index b292dd607dc9..5d9a3a01274d 100644 --- a/src/journeys/lib/get-link-data.ts +++ b/src/journeys/lib/get-link-data.ts @@ -1,2 +1 @@ -// Re-export the getLinkData function from learning tracks for journey tracks -export { default } from '@/learning-track/lib/get-link-data' +export { default } from '@/frame/lib/get-link-data' diff --git a/src/journeys/lib/journey-path-resolver.ts b/src/journeys/lib/journey-path-resolver.ts index ec93200dfe8c..b665a16c92d5 100644 --- a/src/journeys/lib/journey-path-resolver.ts +++ b/src/journeys/lib/journey-path-resolver.ts @@ -110,7 +110,7 @@ function normalizeGuidePath(path: string): string { // First ensure we have a leading slash for consistent processing const pathWithSlash = path.startsWith('/') ? path : `/${path}` - // Use the same normalization pattern as learning tracks and other middleware + // Use the same normalization pattern as other middleware const withoutVersion = getPathWithoutVersion(pathWithSlash) const withoutLanguage = getPathWithoutLanguage(withoutVersion) diff --git a/src/landings/README.md b/src/landings/README.md index 29e5d4053fe0..f14e1a93e124 100644 --- a/src/landings/README.md +++ b/src/landings/README.md @@ -18,7 +18,6 @@ Landing pages serve as navigational hubs that provide a hierarchical view of the | Landing Page Type | Layout Value | Purpose | |-------------------|--------------|---------| | Product landing | `product-landing` | Product overview pages with featured links and release notes | -| Product guides | `product-guides` | Product guides listing organized by categories | | Category landing | `category-landing` | Category pages with hierarchical navigation | | Table of contents | `toc-landing` | Table of contents pages | | Journey landing | `journey-landing` | Guided learning journey pages with track navigation | @@ -43,7 +42,6 @@ npm run test -- src/landings/tests ### Dependencies - [`@/frame`](../frame/README.md) - Context object, page data, shared components - [`@/content-render`](../content-render/README.md) - Renders Liquid in featured link titles -- [`@/learning-track`](../learning-track/README.md) - Learning track data resolution - [`@/journeys`](../journeys/README.md) - Journey track components and data - [`@/products`](../products/README.md) - Product metadata and groupings - [`@/versions`](../versions/README.md) - Version-aware content filtering diff --git a/src/landings/components/ArticleCard.tsx b/src/landings/components/ArticleCard.tsx deleted file mode 100644 index c8aeb39e82ee..000000000000 --- a/src/landings/components/ArticleCard.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ArticleGuide } from '@/landings/components/ProductGuidesContext' -import { Link } from '@/frame/components/Link' - -type Props = { - card: ArticleGuide - typeLabel: string -} - -export const ArticleCard = ({ card, typeLabel }: Props) => { - return ( -
  • - -

    {card.title}

    -
    - {typeLabel} -
    -

    {card.intro}

    - -
  • - ) -} diff --git a/src/landings/components/ArticleCards.tsx b/src/landings/components/ArticleCards.tsx deleted file mode 100644 index c229f9458ce2..000000000000 --- a/src/landings/components/ArticleCards.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useRef } from 'react' - -import { useProductGuidesContext } from '@/landings/components/ProductGuidesContext' -import { useTranslation } from '@/languages/components/useTranslation' -import { ArticleCard } from './ArticleCard' - -export const ArticleCards = () => { - const { t, tObject } = useTranslation('product_guides') - const guideTypes = tObject('guide_types') - const { includeGuides } = useProductGuidesContext() - const articleCardRef = useRef(null) - - const guides = includeGuides || [] - - return ( -
    -
    -
    - {guides.length === 0 - ? t('guides_found.none') - : guides.length === 1 - ? t('guides_found.one') - : t('guides_found.multiple').replace('{n}', `${guides.length}`)} -
    -
    - -
      - {guides.map((card, i) => { - return ( - - ) - })} -
    -
    - ) -} diff --git a/src/landings/components/GuidesHero.tsx b/src/landings/components/GuidesHero.tsx deleted file mode 100644 index 983375aeb91b..000000000000 --- a/src/landings/components/GuidesHero.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useProductGuidesContext } from '@/landings/components/ProductGuidesContext' -import { Lead } from '@/frame/components/ui/Lead' - -export function GuidesHero() { - const { title, intro } = useProductGuidesContext() - - return ( -
    -
    -
    -

    {title}

    - {intro && {intro}} -
    -
    -
    - ) -} diff --git a/src/landings/components/LandingHero.module.scss b/src/landings/components/LandingHero.module.scss deleted file mode 100644 index 89a37ef8ff76..000000000000 --- a/src/landings/components/LandingHero.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.videoWrapper { - padding-bottom: 56.25%; -} diff --git a/src/landings/components/LandingHero.tsx b/src/landings/components/LandingHero.tsx index 1b0bad722ecf..e727bb51b62c 100644 --- a/src/landings/components/LandingHero.tsx +++ b/src/landings/components/LandingHero.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from 'react' +import React from 'react' import cx from 'classnames' import { useRouter } from 'next/router' -import { LinkExternalIcon, NoteIcon } from '@primer/octicons-react' +import { LinkExternalIcon } from '@primer/octicons-react' import { Link } from '@/frame/components/Link' import { useProductLandingContext } from '@/landings/components/ProductLandingContext' @@ -9,29 +9,13 @@ import { useTranslation } from '@/languages/components/useTranslation' import { useVersion } from '@/versions/components/useVersion' import { Lead } from '@/frame/components/ui/Lead' -import styles from './LandingHero.module.scss' - export const LandingHero = () => { - const { - productVideo, - productVideoTranscript, - shortTitle, - title, - beta_product, - intro, - introLinks, - } = useProductLandingContext() + const { title, beta_product, intro, introLinks } = useProductLandingContext() const { t } = useTranslation(['product_landing']) - const [renderIFrame, setRenderIFrame] = useState(false) - - // delay iFrame rendering so that dom ready happens sooner - useEffect(() => { - setRenderIFrame(true) - }, []) return (
    -
    +

    {title}{' '} {beta_product && Beta} @@ -62,29 +46,6 @@ export const LandingHero = () => { })}

    - - {productVideo && ( -
    -
    - -
    - {productVideoTranscript && ( -
    - - - {t('view_transcript')} - -
    - )} -
    - )}
    ) } diff --git a/src/landings/components/ProductGuides.tsx b/src/landings/components/ProductGuides.tsx deleted file mode 100644 index 21d895ba71b9..000000000000 --- a/src/landings/components/ProductGuides.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { DefaultLayout } from '@/frame/components/DefaultLayout' -import { useProductGuidesContext } from '@/landings/components/ProductGuidesContext' -import { LandingSection } from '@/landings/components/LandingSection' -import { GuidesHero } from '@/landings/components/GuidesHero' -import { LearningTracks } from '@/learning-track/components/guides/LearningTracks' -import { ArticleCards } from '@/landings/components/ArticleCards' -import { useTranslation } from '@/languages/components/useTranslation' -import { useMainContext } from '@/frame/components/context/MainContext' -import { UtmPreserver } from '@/frame/components/UtmPreserver' - -export const ProductGuides = () => { - const { title, learningTracks, includeGuides } = useProductGuidesContext() - const { t } = useTranslation('product_guides') - - const { currentProductName } = useMainContext() - - const productName = currentProductName || title - const nameRegex = /{{\s*name\s*}}/ - - return ( - - - - - - -
    - {learningTracks && learningTracks.length > 0 && ( - - - - )} - - {includeGuides && ( - - - - )} -
    -
    - ) -} diff --git a/src/landings/components/ProductGuidesContext.tsx b/src/landings/components/ProductGuidesContext.tsx index 9a490a5fb1a7..0a9df2552a5a 100644 --- a/src/landings/components/ProductGuidesContext.tsx +++ b/src/landings/components/ProductGuidesContext.tsx @@ -1,27 +1,8 @@ import { createContext, useContext } from 'react' -import pick from 'lodash/pick' -import type { ExtendedRequest } from '@/types' - -export type LearningTrack = { - trackName: string - trackProduct: string - title: string - description: string - guides?: Array<{ href: string; contentType: string | null; title: string; intro: string }> -} - -export type ArticleGuide = { - href: string - title: string - intro: string - contentType: string -} export type ProductGuidesContextT = { title: string intro: string - learningTracks: Array - includeGuides?: Array } export const ProductGuidesContext = createContext(null) @@ -37,46 +18,3 @@ export const useProductGuidesContext = (): ProductGuidesContextT => { return context } - -export const getProductGuidesContextFromRequest = (req: ExtendedRequest): ProductGuidesContextT => { - if (!req.context || !req.context.page) { - throw new Error('Request context or page is missing') - } - - const page = req.context.page as typeof req.context.page & { - learningTracks?: Array> - includeGuides?: Array> - } - - const learningTracks: LearningTrack[] = (page.learningTracks || []).map( - (track: Record) => ({ - title: (track.title as string) || '', - description: (track.description as string) || '', - trackName: (track.trackName as string) || '', - trackProduct: (track.trackProduct as string) || '', - guides: ((track.guides as Array>) || []).map( - (guide: Record) => ({ - title: (guide.title as string) || '', - intro: (guide.intro as string) || '', - href: (guide.href as string) || '', - contentType: ((guide.page as any)?.contentType as string) || null, - }), - ), - }), - ) - - return { - ...pick(page, ['title', 'intro']), - title: page.title || '', - intro: page.intro || '', - learningTracks, - includeGuides: (page.includeGuides || []).map((guide: Record) => { - return { - href: (guide.href as string) || '', - title: (guide.title as string) || '', - intro: (guide.intro as string) || '', - contentType: (guide.contentType as string) || '', - } - }), - } -} diff --git a/src/landings/components/ProductLandingContext.tsx b/src/landings/components/ProductLandingContext.tsx index edd65a6daa0c..857e01c54190 100644 --- a/src/landings/components/ProductLandingContext.tsx +++ b/src/landings/components/ProductLandingContext.tsx @@ -37,8 +37,6 @@ export type ProductLandingContextT = { beta_product: boolean product: Product introLinks: Record | null - productVideo: string - productVideoTranscript: string heroImage?: string featuredLinks: Record> productUserExamples: Array<{ username: string; description: string }> @@ -95,10 +93,6 @@ export const getProductLandingContextFromRequest = async ( const page = req.context.page const hasGuidesPage = (page.children || []).includes('/guides') - const productVideo = page.product_video - ? await page.renderProp('product_video', req.context, { textOnly: true }) - : '' - const title = await page.renderProp('title', req.context, { textOnly: true }) const shortTitle = (await page.renderProp('shortTitle', req.context, { textOnly: true })) || null @@ -112,8 +106,6 @@ export const getProductLandingContextFromRequest = async ( title, shortTitle, ...pick(page, ['introPlainText', 'beta_product', 'intro']), - productVideo, - productVideoTranscript: page.product_video_transcript || null, heroImage: page.heroImage || null, hasGuidesPage, product: { @@ -139,13 +131,13 @@ export const getProductLandingContextFromRequest = async ( featuredArticles: Object.entries(req.context.featuredLinks || []) .filter(([key]) => { - return key === 'startHere' || key === 'popular' || key === 'videos' + return key === 'startHere' || key === 'popular' }) .map(([key, links]: any) => { return { key, label: - key === 'popular' || key === 'videos' + key === 'popular' ? req.context.page.featuredLinks[`${key}Heading`] || req.context.site.data.ui.toc[key] : req.context.site.data.ui.toc[key], viewAllHref: diff --git a/src/landings/components/TocLanding.tsx b/src/landings/components/TocLanding.tsx index 0227af5a82fc..67306e8b1a11 100644 --- a/src/landings/components/TocLanding.tsx +++ b/src/landings/components/TocLanding.tsx @@ -11,7 +11,6 @@ import { ArticleList } from '@/landings/components/ArticleList' import { ArticleGridLayout } from '@/frame/components/article/ArticleGridLayout' import { PermissionsStatement } from '@/frame/components/ui/PermissionsStatement' import { Lead } from '@/frame/components/ui/Lead' -import { LearningTrackNav } from '@/learning-track/components/article/LearningTrackNav' import { ClientSideRedirects } from '@/rest/components/ClientSideRedirects' import { RestRedirect } from '@/rest/components/RestRedirect' import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs' @@ -27,7 +26,6 @@ export const TocLanding = () => { variant, featuredLinks, renderedPage, - currentLearningTrack, permissions, } = useTocLandingContext() const { t } = useTranslation('toc') @@ -80,12 +78,6 @@ export const TocLanding = () => {
    - - {currentLearningTrack?.trackName ? ( -
    - -
    - ) : null}
    ) diff --git a/src/landings/components/journey/JourneyLanding.tsx b/src/landings/components/journey/JourneyLanding.tsx index b234608792f7..dddf36be2163 100644 --- a/src/landings/components/journey/JourneyLanding.tsx +++ b/src/landings/components/journey/JourneyLanding.tsx @@ -5,7 +5,8 @@ import { JourneyLearningTracks } from './JourneyLearningTracks' import { UtmPreserver } from '@/frame/components/UtmPreserver' export const JourneyLanding = () => { - const { title, intro, heroImage, introLinks, journeyTracks } = useLandingContext() + const { title, intro, heroImage, introLinks, journeyTracks, journeyArticlesHeading } = + useLandingContext() return ( @@ -14,7 +15,10 @@ export const JourneyLanding = () => {
    - +
    diff --git a/src/landings/components/journey/JourneyLearningTracks.tsx b/src/landings/components/journey/JourneyLearningTracks.tsx index dc9e456a9fdd..2808360b491b 100644 --- a/src/landings/components/journey/JourneyLearningTracks.tsx +++ b/src/landings/components/journey/JourneyLearningTracks.tsx @@ -8,9 +8,10 @@ import styles from './JourneyLearningTracks.module.scss' type JourneyLearningTracksProps = { tracks: JourneyTrack[] + articlesHeading?: string | null } -export const JourneyLearningTracks = ({ tracks }: JourneyLearningTracksProps) => { +export const JourneyLearningTracks = ({ tracks, articlesHeading }: JourneyLearningTracksProps) => { const { t } = useTranslation('journey_landing') if (!tracks || tracks.length === 0) { @@ -55,11 +56,12 @@ export const JourneyLearningTracks = ({ tracks }: JourneyLearningTracksProps) => // simple single journey if (tracks.length === 1) { const track = tracks[0] + const headingText = articlesHeading || t('articles_heading') return (
    -

    {t('articles_heading')}

    +

    {headingText}

      {(track.guides || []).map((article: { href: string; title: string }) => ( diff --git a/src/landings/context/LandingContext.tsx b/src/landings/context/LandingContext.tsx index 1ca8afb1e845..7f4258c2a910 100644 --- a/src/landings/context/LandingContext.tsx +++ b/src/landings/context/LandingContext.tsx @@ -2,7 +2,7 @@ import { createContext, useContext } from 'react' import { getFeaturedLinksFromReq } from '@/landings/components/ProductLandingContext' import { mapRawTocItemToTocItem } from '@/landings/types' import type { TocItem } from '@/landings/types' -import type { ExtendedRequest, Context, LearningTrack } from '@/types' +import type { ExtendedRequest, Context } from '@/types' import type { JourneyTrack } from '@/journeys/lib/journey-path-resolver' import type { FeaturedLink } from '@/landings/components/ProductLandingContext' @@ -18,7 +18,6 @@ export type LandingContextT = { variant?: 'compact' | 'expanded' featuredLinks: Record> renderedPage: string - currentLearningTrack?: LearningTrack | null currentLayout: string heroImage?: string // For landing pages with carousels @@ -29,6 +28,7 @@ export type LandingContextT = { introLinks?: Record | null // For journey landing pages journeyTracks?: JourneyTrack[] + journeyArticlesHeading?: string | null // For article grid category filtering includedCategories?: string[] } @@ -42,6 +42,7 @@ type LandingPage = NonNullable & { rawPermissions?: string introLinks?: Record | null resolvedJourneyTracks?: JourneyTrack[] + journeyArticlesHeading?: string } export const LandingContext = createContext(null) @@ -103,12 +104,12 @@ export const getLandingContextFromRequest = async ( variant: context.genericTocFlat ? 'expanded' : 'compact', featuredLinks: getFeaturedLinksFromReq(req), renderedPage: context.renderedPage ?? '', - currentLearningTrack: context.currentLearningTrack ?? null, currentLayout: context.currentLayoutName ?? '', heroImage: page.heroImage || '/assets/images/banner-images/hero-1', introLinks: page.introLinks || null, carousels, journeyTracks, + journeyArticlesHeading: page.journeyArticlesHeading || null, includedCategories: page.includedCategories || [], } } diff --git a/src/landings/middleware/featured-links.ts b/src/landings/middleware/featured-links.ts index eed889186858..5b392600fd1e 100644 --- a/src/landings/middleware/featured-links.ts +++ b/src/landings/middleware/featured-links.ts @@ -1,8 +1,7 @@ import type { Response, NextFunction } from 'express' import type { ExtendedRequest, FeaturedLinkExpanded } from '@/types' -import getLinkData from '@/learning-track/lib/get-link-data' -import { renderContent } from '@/content-render/index' +import getLinkData from '@/frame/lib/get-link-data' /** * This is the max. number of featured links, by any category, that we @@ -47,47 +46,21 @@ export default async function featuredLinks( req.context.featuredLinks = {} for (const key in req.context.page.featuredLinks) { - if (key === 'videos') { - // Videos are external URLs so don't run through getLinkData, they're - // objects with title and href properties. - // When the title contains Liquid versioning tags, it will be either - // the provided string title or an empty title. When the title is empty, - // it indicates the video is not versioned for the current version - req.context.featuredLinks[key] = [] - if (!(key in req.context.page.featuredLinks)) - throw new Error('featureLinks key not found in Page') - for (const featuredLink of req.context.page.featuredLinks[key]!) { - const title = await renderContent(featuredLink.title, req.context, { - textOnly: true, - }) - const item = { title, href: featuredLink.href } + const pageFeaturedLink = req.context.page.featuredLinks[key] + // Handle different types of featuredLinks by converting to string array + const stringLinks = Array.isArray(pageFeaturedLink) + ? pageFeaturedLink.map((item) => (typeof item === 'string' ? item : item.href)) + : [] - if (item.title) { - req.context.featuredLinks[key].push(item) - if (req.context.featuredLinks[key].length >= MAX_FEATURED_LINKS) { - break - } - } - } - } else { - if (!(key in req.context.page.featuredLinks)) - throw new Error('featureLinks key not found in Page') - const pageFeaturedLink = req.context.page.featuredLinks[key] - // Handle different types of featuredLinks by converting to string array - const stringLinks = Array.isArray(pageFeaturedLink) - ? pageFeaturedLink.map((item) => (typeof item === 'string' ? item : item.href)) - : [] - - const linkData = await getLinkData( - stringLinks, - req.context, - { title: true, intro: true, fullTitle: true }, - MAX_FEATURED_LINKS, - ) - // We need to use a type assertion here because the Page interfaces are incompatible - // between our local types and the global types, but the actual runtime objects are compatible - req.context.featuredLinks[key] = (linkData || []) as unknown as FeaturedLinkExpanded[] - } + const linkData = await getLinkData( + stringLinks, + req.context, + { title: true, intro: true, fullTitle: true }, + MAX_FEATURED_LINKS, + ) + // We need to use a type assertion here because the Page interfaces are incompatible + // between our local types and the global types, but the actual runtime objects are compatible + req.context.featuredLinks[key] = (linkData || []) as unknown as FeaturedLinkExpanded[] } return next() diff --git a/src/landings/pages/product.tsx b/src/landings/pages/product.tsx index 3abba97c46b7..038c7b2b8c03 100644 --- a/src/landings/pages/product.tsx +++ b/src/landings/pages/product.tsx @@ -20,11 +20,6 @@ import { ProductLandingContextT, ProductLandingContext, } from '@/landings/components/ProductLandingContext' -import { - getProductGuidesContextFromRequest, - ProductGuidesContextT, - ProductGuidesContext, -} from '@/landings/components/ProductGuidesContext' import { getArticleContextFromRequest, @@ -34,7 +29,6 @@ import { import { ArticlePage } from '@/frame/components/article/ArticlePage' import { ProductLanding } from '@/landings/components/ProductLanding' -import { ProductGuides } from '@/landings/components/ProductGuides' import { TocLanding } from '@/landings/components/TocLanding' import { CategoryLanding } from '@/landings/components/CategoryLanding' import { @@ -65,7 +59,6 @@ function initiateArticleScripts() { type Props = { mainContext: MainContextT productLandingContext?: ProductLandingContextT - productGuidesContext?: ProductGuidesContextT tocLandingContext?: TocLandingContextT articleContext?: ArticleContextT categoryLandingContext?: CategoryLandingContextT @@ -76,7 +69,6 @@ type Props = { const GlobalPage = ({ mainContext, productLandingContext, - productGuidesContext, tocLandingContext, articleContext, categoryLandingContext, @@ -120,12 +112,6 @@ const GlobalPage = ({ ) - } else if (productGuidesContext) { - content = ( - - - - ) } else if (categoryLandingContext) { content = ( @@ -191,26 +177,17 @@ export const getServerSideProps: GetServerSideProps = async (context) => } else if (currentLayoutName === 'product-landing') { props.productLandingContext = await getProductLandingContextFromRequest(req) additionalUINamespaces.push('product_landing') - } else if (currentLayoutName === 'product-guides') { - props.productGuidesContext = getProductGuidesContextFromRequest(req) - additionalUINamespaces.push('product_guides', 'product_landing') } else if (relativePath?.endsWith('index.md')) { if (currentLayoutName === 'category-landing') { props.categoryLandingContext = getCategoryLandingContextFromRequest(req) } else { props.tocLandingContext = getTocLandingContextFromRequest(req) - if (props.tocLandingContext.currentLearningTrack?.trackName) { - additionalUINamespaces.push('learning_track_nav') - } } } else if (props.mainContext.page) { // All articles that might have hover cards needs this additionalUINamespaces.push('popovers') props.articleContext = getArticleContextFromRequest(req) - if (props.articleContext.currentLearningTrack?.trackName) { - additionalUINamespaces.push('learning_track_nav') - } if (props.articleContext.currentJourneyTrack?.trackId) { additionalUINamespaces.push('journey_track_nav') } diff --git a/src/learning-track/README.md b/src/learning-track/README.md deleted file mode 100644 index 6f444c733f28..000000000000 --- a/src/learning-track/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Learning Tracks - -This module manages "Learning Tracks"—curated, ordered lists of articles that guide a user through a specific topic or goal. When a user enters a learning track, they see a persistent banner or navigation element that helps them move sequentially through the guides. - -## Purpose & Scope - -The goal of this feature is to: -- **Group Content**: Logically group related articles into a sequence. -- **Guide Users**: Provide "Next" and "Previous" navigation context to keep users on the path. -- **Track Progress**: Visually indicate where the user is within the sequence (e.g., "Step 2 of 5"). - -## Architecture - -### Data Source - -Learning tracks are defined in YAML files located in `data/learning-tracks`. -- **Structure**: `data/learning-tracks/.yml` -- **Schema**: Each file contains multiple tracks, where each track is defined as a top-level key with properties like: - - `title`: Display title of the track. - - `description`: Short summary. - - `guides`: An ordered list of relative paths to the articles. - - `versions`: (Optional) Which versions this track applies to. - -### Middleware - -The core logic for *active* tracking happens in `src/learning-track/middleware/learning-track.ts`. -- **Trigger**: It looks for the `?learn=` query parameter in the URL. -- **Context Injection**: If a valid track is found: - - It calculates the current position (index) based on the current page path. - - It resolves the `prevGuide` and `nextGuide` links. - - It injects a `currentLearningTrack` object into `req.context`, which the UI components use to render the progress banner. - -### Library - -`src/learning-track/lib/process-learning-tracks.ts` is used (often by landing pages) to list available tracks. -- **Rendering**: It handles Liquid rendering for titles and descriptions. -- **Versioning**: It filters out tracks or specific guides that don't exist in the current documentation version. -- **Translation Fallback**: It ensures that if a translated track has broken data, it falls back to the English structure (guides list) while keeping translated titles if possible. - -## Setup & Usage - -### Defining a Track - -Add a track to `data/learning-tracks/YOUR_PRODUCT.yml`: - -```yaml -getting_started: - title: "Getting Started with GitHub" - description: "Learn the basics of repositories and commits." - versions: - fpt: '*' - ghec: '*' - guides: - - /get-started/signing-up-for-github/signing-up-for-a-new-github-account - - /get-started/first-steps-with-git/set-up-git -``` - -### Linking to a Track - -To start a user on a track, link to the first guide with the `learn` parameter (matching the track key defined in the YAML): -`/en/get-started/signing-up-for-github/signing-up-for-a-new-github-account?learn=getting_started` - -### URL Structure - -- **`learn=`**: Identifies the active track. -- **`learnProduct=`**: (Optional) Used when the track belongs to a different product than the current page context. - -## Dependencies - -- **`data/learning-tracks`**: The source of truth for track definitions. -- **`src/content-render`**: Used to render Liquid within track titles and descriptions. -- **`src/versions`**: Used to filter tracks based on the current page version. - -## Ownership - -- **Team**: `@github/docs-engineering` -- **Content Owners**: The Writers and Content Strategists who define the tracks in the `data` directory. - -## Current State & Known Issues - -- **Query Param Persistence**: The feature relies on the `?learn=` parameter persisting as the user clicks links. If a user navigates away via a link that doesn't preserve query params (e.g., a standard markdown link outside the track navigation), they "fall off" the track. -- **Translation Sync**: Translators sometimes translate the `guides` list paths or break the YAML structure. The code has specific logic to force-use English guide lists to prevent 404s in translated versions. \ No newline at end of file diff --git a/src/learning-track/components/article/LearningTrackCard.tsx b/src/learning-track/components/article/LearningTrackCard.tsx deleted file mode 100644 index 3a02d325fe49..000000000000 --- a/src/learning-track/components/article/LearningTrackCard.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useRouter } from 'next/router' - -import { Link } from '@/frame/components/Link' -import type { LearningTrack } from '@/frame/components/context/ArticleContext' -import { useTranslation } from '@/languages/components/useTranslation' - -type Props = { - track: LearningTrack -} -export function LearningTrackCard({ track }: Props) { - const { locale } = useRouter() - const { t } = useTranslation('learning_track_nav') - const { trackTitle, trackName, nextGuide, trackProduct, numberOfGuides, currentGuideIndex } = - track - return ( -
      -
      -

      - - {trackTitle} - -

      - - {t('current_progress') - .replace('{n}', `${numberOfGuides}`) - .replace('{i}', `${currentGuideIndex + 1}`)} - -
      - - {nextGuide ? ( - <> - {t('next_guide')}: - - {nextGuide.title} - - - ) : ( - - {t('more_guides')} - - )} - -
      -
      - ) -} diff --git a/src/learning-track/components/article/LearningTrackNav.tsx b/src/learning-track/components/article/LearningTrackNav.tsx deleted file mode 100644 index 6c3a776bb021..000000000000 --- a/src/learning-track/components/article/LearningTrackNav.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Link } from '@/frame/components/Link' -import type { LearningTrack } from '@/frame/components/context/ArticleContext' -import { useTranslation } from '@/languages/components/useTranslation' - -type Props = { - track: LearningTrack -} -export function LearningTrackNav({ track }: Props) { - const { t } = useTranslation('learning_track_nav') - const { prevGuide, nextGuide, trackName, trackProduct } = track - return ( -
      - - {prevGuide && ( - <> - {t('prev_guide')} - - {prevGuide.title} - - - )} - - - - {nextGuide && ( - <> - {t('next_guide')} - - {nextGuide.title} - - - )} - -
      - ) -} diff --git a/src/learning-track/components/guides/LearningTrack.tsx b/src/learning-track/components/guides/LearningTrack.tsx deleted file mode 100644 index 83422a5d9c3f..000000000000 --- a/src/learning-track/components/guides/LearningTrack.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useTranslation } from '@/languages/components/useTranslation' -import { ArrowRightIcon } from '@primer/octicons-react' -import { LearningTrack as LearningTrackT } from '@/landings/components/ProductGuidesContext' -import { Link } from '@/frame/components/Link' - -import { HeadingLink } from '@/frame/components/article/HeadingLink' - -type Props = { - track: LearningTrackT -} - -export const LearningTrack = ({ track }: Props) => { - const { t, tObject } = useTranslation('product_guides') - if (!track) return
      - - return ( -
      - {track.title} -

      {track.description}

      - - - {t('start_path')} - - - - {track.guides && ( -
        - {track.guides.map((guide) => ( -
      1. - - {tObject('guide_types')[guide.contentType || ''] as string} - - - {guide.title} - -
      2. - ))} -
      - )} -
      - ) -} diff --git a/src/learning-track/components/guides/LearningTracks.tsx b/src/learning-track/components/guides/LearningTracks.tsx deleted file mode 100644 index c1b8b4d2eda2..000000000000 --- a/src/learning-track/components/guides/LearningTracks.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useProductGuidesContext } from '@/landings/components/ProductGuidesContext' -import { LearningTrack } from '@/learning-track/components/guides/LearningTrack' - -export const LearningTracks = () => { - const { learningTracks } = useProductGuidesContext() - - return ( -
      - {learningTracks.map((track) => { - return - })} -
      - ) -} diff --git a/src/learning-track/lib/process-learning-tracks.ts b/src/learning-track/lib/process-learning-tracks.ts deleted file mode 100644 index cd1ff360aa53..000000000000 --- a/src/learning-track/lib/process-learning-tracks.ts +++ /dev/null @@ -1,117 +0,0 @@ -import getLinkData from './get-link-data' -import getApplicableVersions from '@/versions/lib/get-applicable-versions' -import { getDataByLanguage } from '@/data-directory/lib/get-data' -import { renderContent } from '@/content-render/index' -import { executeWithFallback } from '@/languages/lib/render-with-fallback' -import { - Context, - TrackGuide, - LearningTrack, - LearningTrackMetadata, - ProcessedLearningTracks, -} from './types' - -const renderOpts = { textOnly: true } - -// This module returns an object that contains a single featured learning track -// and an array of all the other learning tracks for the current version. -export default async function processLearningTracks( - rawLearningTracks: string[], - context: Context, -): Promise { - const learningTracks: LearningTrack[] = [] - - if (!context.currentProduct) { - throw new Error(`Missing context.currentProduct value.`) - } - - for (const rawTrackName of rawLearningTracks) { - // Track names in frontmatter may include Liquid conditionals. - const renderedTrackName = rawTrackName.includes('{') - ? await executeWithFallback( - context, - () => renderContent(rawTrackName, context, renderOpts), - (enContext: Context) => renderContent(rawTrackName, enContext, renderOpts), - ) - : rawTrackName - if (!renderedTrackName) continue - - // Find the data for the current product and track name. - - if (context.currentProduct.includes('.')) { - throw new Error(`currentProduct cannot contain a . (${context.currentProduct})`) - } - if (renderedTrackName.includes('.')) { - throw new Error(`renderedTrackName cannot contain a . (${renderedTrackName})`) - } - - // Note: this will use the translated learning tracks and automatically - // fall back to English if they don't exist on disk in the translation. - const rawTrack = getDataByLanguage( - `learning-tracks.${context.currentProduct}.${renderedTrackName}`, - context.currentLanguage!, - ) - if (!rawTrack) { - throw new Error(`No learning track called '${renderedTrackName}'.`) - } - - // For translated tracks, always use English `guides` and `versions` - // (translations sometimes break Liquid in those fields). Keep translated - // `title` and `description` so we can render them with a fallback. - let enTrack!: LearningTrackMetadata - let track: LearningTrackMetadata - if (context.currentLanguage !== 'en') { - enTrack = getDataByLanguage( - `learning-tracks.${context.currentProduct}.${renderedTrackName}`, - 'en', - ) - track = { - ...rawTrack, - guides: enTrack.guides, - versions: enTrack.versions, - } - } else { - track = rawTrack - } - - // If there is no `versions` prop in the learning track frontmatter, assume the track should display in all versions. - if (track.versions) { - const trackVersions = getApplicableVersions(track.versions as string) - - // If the current version is not included, do not display the track. - if (!context.currentVersion || !trackVersions.includes(context.currentVersion)) { - continue - } - } - - const title = await executeWithFallback( - context, - () => renderContent(track.title, context, renderOpts), - (enContext: Context) => renderContent(enTrack.title, enContext, renderOpts), - ) - const description = await executeWithFallback( - context, - () => renderContent(track.description, context, renderOpts), - (enContext: Context) => renderContent(enTrack.description, enContext, renderOpts), - ) - - const guides = (await getLinkData(track.guides, context)) || [] - - const learningTrack: LearningTrack = { - trackName: renderedTrackName, - trackProduct: context.currentProduct || null, - title, - description, - // getLinkData respects versioning and only returns guides available in the current version; - // if no guides are available, the learningTrack.guides property will be an empty array. - guides: guides as TrackGuide[], - } - - // Only add the track to the array of tracks if there are guides in this version and it's not the featured track. - if (Array.isArray(learningTrack.guides) && learningTrack.guides.length > 0) { - learningTracks.push(learningTrack) - } - } - - return { learningTracks } -} diff --git a/src/learning-track/lib/types.ts b/src/learning-track/lib/types.ts deleted file mode 100644 index 60ab19d011ee..000000000000 --- a/src/learning-track/lib/types.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Common types used across learning track components - */ - -import type { Context, Page as MainPage } from '@/types' - -// Re-export Context from main types to avoid duplicate definitions -export type { Context } - -/** - * Options for retrieving link data - */ -export interface LinkOptions { - title?: boolean - intro?: boolean - fullTitle?: boolean -} - -/** - * Result of processing a link - */ -export interface ProcessedLink { - href: string - page: Page - title?: string - fullTitle?: string - intro?: string -} - -/** - * Definitions for featured links data - */ -export interface FeaturedLink { - title: string - href: string -} - -export interface PageFeaturedLinks { - [key: string]: string[] | FeaturedLink[] -} - -/** - * Page interface for basic page properties - * Using the main Page type from @/types - */ -export type Page = MainPage - -/** - * Guide in a learning track - */ -export interface TrackGuide { - href: string - page: Page - title: string - intro?: string -} - -/** - * A processed learning track - */ -export interface LearningTrack { - trackName: string - trackProduct: string | null - title: string - description: string - guides: TrackGuide[] -} - -/** - * Learning track metadata with guides - */ -export interface LearningTrackMetadata { - title: string - description: string - guides: string[] - versions?: unknown -} - -/** - * Collection of learning tracks by product and track name - */ -export interface LearningTracks { - [productId: string]: { - [trackName: string]: LearningTrackMetadata - } -} - -/** - * Return type for processLearningTracks function - */ -export interface ProcessedLearningTracks { - learningTracks: LearningTrack[] -} - -/** - * Learning track data for the current guide - */ -export interface CurrentLearningTrack { - trackName: string - trackProduct: string - trackTitle: string - numberOfGuides?: number - currentGuideIndex?: number - nextGuide?: { - href: string - title: string | undefined - } - prevGuide?: { - href: string - title: string | undefined - } -} diff --git a/src/learning-track/middleware/learning-track.ts b/src/learning-track/middleware/learning-track.ts deleted file mode 100644 index 82a54cf89fc8..000000000000 --- a/src/learning-track/middleware/learning-track.ts +++ /dev/null @@ -1,201 +0,0 @@ -import type { Response, NextFunction } from 'express' - -import type { ExtendedRequest, LearningTracks } from '@/types' -import type { Context, CurrentLearningTrack, TrackGuide } from '../lib/types' -import { getPathWithoutLanguage, getPathWithoutVersion } from '@/frame/lib/path-utils' -import getLinkData from '../lib/get-link-data' -import { renderContent } from '@/content-render/index' -import { executeWithFallback } from '@/languages/lib/render-with-fallback' -import { getDeepDataByLanguage } from '@/data-directory/lib/get-data' - -export default async function learningTrack( - req: ExtendedRequest & { context: Context }, - res: Response, - next: NextFunction, -) { - if (!req.context) throw new Error('request is not contextualized') - - const noTrack = () => { - req.context.currentLearningTrack = null - return next() - } - - if (!req.context.page) return next() - - if (!req.query.learn) return noTrack() - if (Array.isArray(req.query.learn)) return noTrack() - const trackName = req.query.learn as string - - let trackProduct = req.context.currentProduct as string - const rawLearningTracks = getDeepDataByLanguage( - 'learning-tracks', - req.language!, - ) as LearningTracks - - let allLearningTracks = rawLearningTracks - - if (req.language !== 'en') { - // Don't trust the `.guides` from the translation. It too often has - // broken Liquid (e.g. `{% ifversion fpt 또는 ghec 또는 ghes %}`) - const allEnglishLearningTracks = getDeepDataByLanguage( - 'learning-tracks', - 'en', - ) as LearningTracks - // Build a filtered copy instead of mutating the cached original - const filtered: LearningTracks = {} - for (const [key, tracks] of Object.entries(rawLearningTracks)) { - if (!(key in allEnglishLearningTracks)) { - console.warn('No English learning track for %s', key) - continue - } - filtered[key] = {} - for (const [name, track] of Object.entries(tracks)) { - if (!(name in allEnglishLearningTracks[key])) continue - filtered[key][name] = { - ...track, - guides: allEnglishLearningTracks[key][name].guides, - } - } - } - allLearningTracks = filtered - } - let tracksPerProduct = allLearningTracks[trackProduct] - - // If there are no learning tracks for the current product, try and fall - // back to the learning track product set as a URL parameter. This handles - // the case where a learning track has guide paths for a different product - // than the current learning track product. - if (!tracksPerProduct) { - if (!req.query.learnProduct) return noTrack() - if (Array.isArray(req.query.learnProduct)) { - trackProduct = req.query.learnProduct[0] as string - } else { - trackProduct = req.query.learnProduct as string - } - tracksPerProduct = allLearningTracks[trackProduct] - } - if (!tracksPerProduct) return noTrack() - - const track = allLearningTracks[trackProduct][trackName] - if (!track) return noTrack() - - // The trackTitle comes from a data .yml file and may use Liquid templating, so we need to render it - const renderOpts = { textOnly: true } - // Some translated titles are known to have broken Liquid, so we need to - // try rendering them in English as a fallback. - const trackTitle = await executeWithFallback( - req.context, - () => renderContent(track.title, req.context, renderOpts), - (enContext: Context) => { - const allEnglishLearningTracks = getDeepDataByLanguage( - 'learning-tracks', - 'en', - ) as LearningTracks - const enTrack = allEnglishLearningTracks[trackProduct]?.[trackName] - if (!enTrack) { - throw new Error(`English learning track not found: ${trackProduct}.${trackName}`) - } - return renderContent(enTrack.title, enContext, renderOpts) - }, - ) - - const currentLearningTrack: CurrentLearningTrack = { trackName, trackProduct, trackTitle } - const guidePath = getPathWithoutLanguage(getPathWithoutVersion(req.pagePath)) - - // The raw track.guides will return all guide paths, need to use getLinkData - // so we only get guides available in the current version - const trackGuides = ((await getLinkData(track.guides, req.context)) || []) as TrackGuide[] - - const trackGuidePaths = trackGuides.map((guide) => { - return getPathWithoutLanguage(getPathWithoutVersion(guide.href)) - }) - - let guideIndex = trackGuidePaths.findIndex((path) => path === guidePath) - - // The learning track path may use Liquid version conditionals, handle the - // case where the requested path is a learning track path but won't match - // because of a Liquid conditional. - if (guideIndex < 0) { - guideIndex = await indexOfLearningTrackGuide(trackGuidePaths, guidePath, req.context) - } - - // Also check if the learning track path is now a redirect to the requested - // page, we still want to render the learning track banner in that case. - // Also handles Liquid conditionals in the track path. - if (guideIndex < 0) { - for (const redirect of req.context.page.redirect_from || []) { - if (guideIndex >= 0) break - - guideIndex = await indexOfLearningTrackGuide(trackGuidePaths, redirect, req.context) - } - } - - if (guideIndex < 0) return noTrack() - - currentLearningTrack.numberOfGuides = trackGuidePaths.length - currentLearningTrack.currentGuideIndex = guideIndex - - if (guideIndex > 0) { - const prevGuidePath = trackGuidePaths[guideIndex - 1] - const resultData = await getLinkData(prevGuidePath, req.context, { - title: true, - intro: false, - fullTitle: false, - }) - if (!resultData || !resultData.length) return noTrack() - const result = resultData[0] - - const href = result.href - const title = result.title - currentLearningTrack.prevGuide = { href, title } - } - - if (guideIndex < trackGuidePaths.length - 1) { - const nextGuidePath = trackGuidePaths[guideIndex + 1] - const resultData = await getLinkData(nextGuidePath, req.context, { - title: true, - intro: false, - fullTitle: false, - }) - if (!resultData || !resultData.length) return noTrack() - const result = resultData[0] - - const href = result.href - const title = result.title - - currentLearningTrack.nextGuide = { href, title } - } - - req.context.currentLearningTrack = currentLearningTrack - - return next() -} - -// Find the index of a learning track guide path in an array of guide paths, -// return -1 if not found. -async function indexOfLearningTrackGuide( - trackGuidePaths: string[], - guidePath: string, - context: Context, -) { - let guideIndex = -1 - - const renderOpts = { textOnly: true } - for (let i = 0; i < trackGuidePaths.length; i++) { - // Learning track URLs may have Liquid conditionals. - const renderedGuidePath = await executeWithFallback( - context, - () => renderContent(trackGuidePaths[i], context, renderOpts), - (enContext: Context) => renderContent(trackGuidePaths[i], enContext, renderOpts), - ) - - if (!renderedGuidePath) continue - - if (renderedGuidePath === guidePath) { - guideIndex = i - break - } - } - - return guideIndex -} diff --git a/src/learning-track/tests/lint-data.ts b/src/learning-track/tests/lint-data.ts deleted file mode 100644 index 88edd926be71..000000000000 --- a/src/learning-track/tests/lint-data.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import type { LearningTracks } from '@/types' -import { loadPages, loadPageMap } from '@/frame/lib/page-data' -import loadRedirects from '@/redirects/lib/precompile' -import { getDeepDataByLanguage } from '@/data-directory/lib/get-data' -import { checkURL } from '@/tests/helpers/check-url' - -const pageList = await loadPages(undefined, ['en']) -const pages = await loadPageMap(pageList) -const redirects = await loadRedirects(pageList) - -describe('learning tracks', () => { - const allLearningTracks = getDeepDataByLanguage('learning-tracks', 'en') as LearningTracks - const topLevels = Object.keys(allLearningTracks) - - test.each(topLevels)('learning-track in data/learning-tracks/%s.yml', (topLevel) => { - const learningTracks = allLearningTracks[topLevel] - const redirectsContext = { redirects, pages } - - for (const learningTrack of Object.values(learningTracks)) { - const length = learningTrack.guides.length - const size = new Set(learningTrack.guides).size - let errorMessage = '' - if (length !== size) { - errorMessage = `In data/learning-tracks/${topLevel}.yml there are duplicate guides.` - const counts = new Map() - for (const guide of learningTrack.guides) { - counts.set(guide, (counts.get(guide) || 0) + 1) - } - const dupes = [...counts.entries()].filter(([, count]) => count > 1).map(([entry]) => entry) - errorMessage += `\nTo fix this, remove: ${dupes.join(' and ')}` - } - expect(length, errorMessage).toEqual(size) - } - - type Trouble = { - uri: string - index: number - redirects: string | undefined - } - type TroubleTuple = [string, Trouble[]] - const troubles = Object.entries(learningTracks) - .map(([learningTrackKey, learningTrack]) => { - return [ - learningTrackKey, - learningTrack.guides - .map((guide, i) => checkURL(guide, i, redirectsContext)) - .filter(Boolean), - ] - }) - .filter(([, trouble]) => trouble.length > 0) as TroubleTuple[] - - let errorMessage = `In data/learning-tracks/${topLevel}.yml there are ${troubles.length} guides that are not correct.\n` - let fixables = 0 - for (const [key, guides] of troubles) { - errorMessage += `Under "${key}"...\n` - for (const { uri, index, redirects: redirectTo } of guides) { - if (redirectTo) { - fixables += 1 - errorMessage += ` guide: #${index + 1} ${uri} redirects to ${redirectTo}\n` - } else { - errorMessage += ` guide: #${index + 1} ${uri} is broken.\n` - } - } - } - if (fixables) { - errorMessage += `\nNOTE! To automatically fix the redirects run this command:\n` - errorMessage += `\n\t./src/links/scripts/update-internal-links.ts data/learning-tracks/${topLevel}.yml\n` - } - expect(troubles.length, errorMessage).toEqual(0) - }) -}) diff --git a/src/links/lib/update-internal-links.ts b/src/links/lib/update-internal-links.ts index 54dd3377402a..123aa6e8f2fb 100644 --- a/src/links/lib/update-internal-links.ts +++ b/src/links/lib/update-internal-links.ts @@ -1,5 +1,4 @@ import fs from 'fs' -import path from 'path' import { visit, Test } from 'unist-util-visit' import { fromMarkdown } from 'mdast-util-from-markdown' @@ -83,8 +82,8 @@ async function updateFile( // Since this function can process both `.md` and `.yml` files, // when treating a `.md` file, the `data` from `frontmatter(rawContent)` - // is easy. But when dealing a file like `data/learning-tracks/foo.yml` - // then the `frontmatter(rawContent).data` always becomes `{}`. + // is easy. But when dealing a `.yml` file, + // the `frontmatter(rawContent).data` always becomes `{}`. // And since the Yaml file might contain arrays of internal linked // pathnames, we have to re-read it fully. if (file.endsWith('.yml')) { @@ -110,20 +109,6 @@ async function updateFile( const HAS_LINKS: Record = { featuredLinks: ['gettingStarted', 'startHere', 'guideCards', 'popular'], introLinks: ANY, - includeGuides: IS_ARRAY, - } - - if ( - file.split(path.sep).includes('data') && - file.split(path.sep).includes('learning-tracks') && - file.endsWith('.yml') - ) { - // data/learning-tracks/**/*.yml files are different because the keys - // are arbitrary but what they might all have in common is a key - // there called `guides` - for (const key of Object.keys(data)) { - HAS_LINKS[key] = ['guides'] - } } for (const [key, seek] of Object.entries(HAS_LINKS)) { diff --git a/src/rest/pages/category.tsx b/src/rest/pages/category.tsx index 0a88843cb701..2ba89b966ea2 100644 --- a/src/rest/pages/category.tsx +++ b/src/rest/pages/category.tsx @@ -1,12 +1,7 @@ import { GetServerSideProps } from 'next' import { Operation } from '@/rest/components/types' import { RestReferencePage } from '@/rest/components/RestReferencePage' -import { - addUINamespaces, - getMainContext, - MainContext, - MainContextT, -} from '@/frame/components/context/MainContext' +import { getMainContext, MainContext, MainContextT } from '@/frame/components/context/MainContext' import { AutomatedPageContext, AutomatedPageContextT, @@ -209,9 +204,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => tocLandingContext.tocItems = restCategoryTocItems const mainContext = await getMainContext(req, res) - if (tocLandingContext.currentLearningTrack?.trackName) { - addUINamespaces(req, mainContext.data.ui, ['learning_track_nav']) - } return { props: { diff --git a/src/types/types.ts b/src/types/types.ts index 27884bb0b22b..223327096fd2 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -57,11 +57,7 @@ export type PageFrontmatter = { featuredLinks?: FeaturedLinks changelog?: ChangeLog contentType?: string - includeGuides?: string[] - learningTracks?: string[] beta_product?: boolean - product_video?: boolean - product_video_transcript?: string interactive?: boolean communityRedirect?: { name: string @@ -80,11 +76,6 @@ type FeaturedLinks = { guideCards?: string[] popular?: string[] popularHeading?: string - videos?: { - title: string - href: string - }[] - videoHeadings?: string } export type ChildGroup = { @@ -186,45 +177,11 @@ export type Context = { productUserExamples?: ProductExample[] productGroups?: ProductGroup[] featuredLinks?: FeaturedLinksExpanded - currentLearningTrack?: LearningTrack | null renderedPage?: string miniTocItems?: MiniTocItem[] markdownRequested?: boolean markdownViaUrl?: boolean } -export type LearningTracks = { - [group: string]: { - [track: string]: { - title: string - description: string - versions?: FrontmatterVersions - guides: string[] - } - } -} -export type LearningTrack = { - trackName: string - trackProduct: string - trackTitle: string - numberOfGuides?: number - currentGuideIndex?: number - nextGuide?: { - href: string - title: string | undefined - } - prevGuide?: { - href: string - title: string | undefined - } -} - -export type TrackGuide = { - href: string - page: Page - title: string - intro: string -} - export type FeaturedLinkExpanded = { href: string title: string diff --git a/src/versions/scripts/use-short-versions.ts b/src/versions/scripts/use-short-versions.ts index d308feaea2ca..2ab266c75b2f 100755 --- a/src/versions/scripts/use-short-versions.ts +++ b/src/versions/scripts/use-short-versions.ts @@ -104,7 +104,7 @@ async function main() { for (const file of yamlFiles) { const yamlContent = fs.readFileSync(file, 'utf8') const yamlReplacements = getLiquidReplacements(yamlContent, file) - // Update any `versions` properties in the YAML as well (learning tracks, etc.) + // Update any `versions` properties in the YAML as well const newYamlContent = makeLiquidReplacements(yamlReplacements, yamlContent) .replace(/("|')?free-pro-team("|')?:/g, 'fpt:') .replace(/("|')?enterprise-server("|')?:/g, 'ghes:')