diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 46fa6e2d40bf..b04da84f0758 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -63,7 +63,7 @@ // Lifecycle commands // Start a web server and keep it running - "postStartCommand": "nohup bash -c 'npm start &'", + "postStartCommand": "nohup bash -c 'npm ci && npm start &'", // Set port 4000 to be public "postAttachCommand": "gh cs ports visibility 4000:public -c \"$CODESPACE_NAME\"", diff --git a/content/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens.md b/content/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens.md index 0da4f95c8461..9cc6c1285c71 100644 --- a/content/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens.md +++ b/content/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens.md @@ -144,6 +144,7 @@ Below are some example URLs that generate the tokens we see most often: * [GitHub Models access](https://github.com/settings/personal-access-tokens/new?name=GitHub+Models+token&description=Used%20to%20call%20GitHub%20Models%20APIs%20to%20easily%20run%20LLMs%3A%20https%3A%2F%2Fdocs.github.com%2Fgithub-models%2Fquickstart%23step-2-make-an-api-call&user_models=read) * [Update code and open a PR](https://github.com/settings/personal-access-tokens/new?name=Core-loop+token&description=Write%20code%20and%20push%20it%20to%20main%21%20Includes%20permission%20to%20edit%20workflow%20files%20for%20Actions%20-%20remove%20%60workflows%3Awrite%60%20if%20you%20don%27t%20need%20to%20do%20that&contents=write&pull_requests=write&workflows=write) * [Manage Copilot licenses in an organization](https://github.com/settings/personal-access-tokens/new?name=Core-loop+token&description=Enable%20or%20disable%20copilot%20access%20for%20users%20with%20the%20Seat%20Management%20APIs%3A%20https%3A%2F%2Fdocs.github.com%2Frest%2Fcopilot%2Fcopilot-user-management%0ABe%20sure%20to%20select%20an%20organization%20for%20your%20resource%20owner%20below%21&organization_copilot_seat_management=write) +* [Make Copilot requests](https://github.com/settings/personal-access-tokens/new?name=Copilot+requests+token&description=Make%20Copilot%20API%20requests%20on%20behalf%20of%20the%20user%2C%20consuming%20premium%20requests%3A%20https%3A%2F%2Fdocs.github.com%2Fcopilot%2Fconcepts%2Fbilling%2Fcopilot-requests&copilot_requests=write) #### Supported Query Parameters @@ -173,6 +174,7 @@ Account permissions are only used when the current user is set as the resource o | `codespaces_user_secrets` | Codespaces user secrets | `read`, `write` | | `copilot_messages` | Copilot Chat | `read` | | `copilot_editor_context` | Copilot Editor Context | `read` | +| `copilot_requests` | Copilot requests | `write` | | `emails` | Email addresses | `read`, `write` | | `user_events` | Events | `read` | | `followers` | Followers | `read`, `write` | @@ -189,6 +191,12 @@ Account permissions are only used when the current user is set as the resource o | `starring` | Starring | `read`, `write` | | `watching` | Watching | `read`, `write` | +{% ifversion copilot %} + +> [!NOTE] +> The `copilot_requests` permission enables making {% data variables.product.prodname_copilot_short %} requests for the given user, which count towards the user's premium request allowance or are charged to overage billing if the allowance is exceeded. For more information about {% data variables.product.prodname_copilot_short %} requests and billing, see [AUTOTITLE](/copilot/concepts/billing/copilot-requests). + +{% endif %} ##### Repository Permissions Repository permissions work for both user and organization resource owners. diff --git a/content/code-security/codeql-for-vs-code/getting-started-with-codeql-for-vs-code/index.md b/content/code-security/codeql-for-vs-code/getting-started-with-codeql-for-vs-code/index.md deleted file mode 100644 index b0646f349c81..000000000000 --- a/content/code-security/codeql-for-vs-code/getting-started-with-codeql-for-vs-code/index.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Getting started with the {% data variables.product.prodname_codeql %} for Visual Studio Code extension -shortTitle: Getting started -intro: The {% data variables.product.prodname_codeql %} extension for {% data variables.product.prodname_vscode %} makes it easy to run a query to find problems in codebases. -product: '{% data reusables.gated-features.codeql %}' -versions: - fpt: '*' - ghes: '*' - ghec: '*' -topics: - - Code Security - - Code scanning - - CodeQL -redirect_from: - - /code-security/codeql-for-vs-code/setting-up-codeql-in-visual-studio-code ---- - diff --git a/content/code-security/codeql-for-vs-code/index.md b/content/code-security/codeql-for-vs-code/index.md index eabad0d0f0d4..278e233980e6 100644 --- a/content/code-security/codeql-for-vs-code/index.md +++ b/content/code-security/codeql-for-vs-code/index.md @@ -13,7 +13,6 @@ topics: - Code scanning - CodeQL children: - - /getting-started-with-codeql-for-vs-code - /using-the-advanced-functionality-of-the-codeql-for-vs-code-extension - /troubleshooting-codeql-for-vs-code --- diff --git a/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/browsing-security-advisories-in-the-github-advisory-database.md b/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/browsing-security-advisories-in-the-github-advisory-database.md index 36918298f6a8..8d1f36d5b4ab 100644 --- a/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/browsing-security-advisories-in-the-github-advisory-database.md +++ b/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/browsing-security-advisories-in-the-github-advisory-database.md @@ -11,6 +11,8 @@ redirect_from: - /code-security/dependabot/dependabot-alerts/browsing-security-advisories-in-the-github-advisory-database - /code-security/security-advisories/global-security-advisories/browsing-security-advisories-in-the-github-advisory-database - /code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/browsing-security-advisories-in-the-github-advisory-database + - /code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database + - /code-security/security-advisories/global-security-advisories versions: fpt: '*' ghec: '*' diff --git a/content/code-security/how-tos/scan-code-for-vulnerabilities/scan-from-vs-code/installing-codeql-for-vs-code.md b/content/code-security/how-tos/scan-code-for-vulnerabilities/scan-from-vs-code/installing-codeql-for-vs-code.md index 6e79166cd0ac..18fefb32c1cf 100644 --- a/content/code-security/how-tos/scan-code-for-vulnerabilities/scan-from-vs-code/installing-codeql-for-vs-code.md +++ b/content/code-security/how-tos/scan-code-for-vulnerabilities/scan-from-vs-code/installing-codeql-for-vs-code.md @@ -14,6 +14,8 @@ intro: To get started with {% data variables.product.prodname_codeql %} for {% d allowTitleToDifferFromFilename: true redirect_from: - /code-security/codeql-for-vs-code/getting-started-with-codeql-for-vs-code/installing-codeql-for-vs-code + - /code-security/codeql-for-vs-code/getting-started-with-codeql-for-vs-code + - /code-security/codeql-for-vs-code/setting-up-codeql-in-visual-studio-code contentType: how-tos --- diff --git a/content/code-security/secret-scanning/index.md b/content/code-security/secret-scanning/index.md index 37fba852ed36..8d84c8230d75 100644 --- a/content/code-security/secret-scanning/index.md +++ b/content/code-security/secret-scanning/index.md @@ -19,5 +19,4 @@ children: - /managing-alerts-from-secret-scanning - /using-advanced-secret-scanning-and-push-protection-features - /troubleshooting-secret-scanning-and-push-protection - - /secret-scanning-partnership-program --- diff --git a/content/code-security/secret-scanning/secret-scanning-partnership-program/index.md b/content/code-security/secret-scanning/secret-scanning-partnership-program/index.md deleted file mode 100644 index 78ca972e3b24..000000000000 --- a/content/code-security/secret-scanning/secret-scanning-partnership-program/index.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Secret scanning partnership program -intro: As a service provider, you can partner with {% data variables.product.prodname_dotcom %} to have your secret token formats secured through secret scanning, which searches for accidental commits of your secret format and can be sent to a service provider's verify endpoint. -versions: - fpt: '*' - ghec: '*' -topics: - - API -shortTitle: Partner program ---- - diff --git a/content/code-security/security-advisories/index.md b/content/code-security/security-advisories/index.md index cc2dc92eb229..5c6f099508c4 100644 --- a/content/code-security/security-advisories/index.md +++ b/content/code-security/security-advisories/index.md @@ -2,17 +2,15 @@ title: Working with security advisories shortTitle: Security advisories allowTitleToDifferFromFilename: true -intro: 'Learn how to work with security advisories on {% data variables.product.prodname_dotcom %},{% ifversion fpt or ghec %} whether you want to contribute to an existing global advisory, or create a security advisory for a repository,{% endif %} improving collaboration between repository maintainers and security researchers.' +intro: 'Learn how to work with security advisories on {% data variables.product.prodname_dotcom %}, whether you want to contribute to an existing global advisory, or create a security advisory for a repository, improving collaboration between repository maintainers and security researchers.' versions: fpt: '*' ghec: '*' - ghes: '*' topics: - Security advisories - Vulnerabilities - Repositories - CVEs children: - - /working-with-global-security-advisories-from-the-github-advisory-database - /working-with-repository-security-advisories --- diff --git a/content/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/index.md b/content/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/index.md deleted file mode 100644 index 8cd90aa0ca67..000000000000 --- a/content/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/index.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Working with global security advisories from the GitHub Advisory Database -shortTitle: Global security advisories -intro: Browse the {% data variables.product.prodname_advisory_database %} and submit improvements to any global security advisory. -redirect_from: - - /code-security/security-advisories/global-security-advisories -versions: - fpt: '*' - ghes: '*' - ghec: '*' -topics: - - Security advisories - - Vulnerabilities - - Repositories - - CVEs ---- - diff --git a/content/code-security/tutorials/secret-scanning-partner-program.md b/content/code-security/tutorials/secret-scanning-partner-program.md index 56d627aba7e0..e742e073bd1b 100644 --- a/content/code-security/tutorials/secret-scanning-partner-program.md +++ b/content/code-security/tutorials/secret-scanning-partner-program.md @@ -9,6 +9,7 @@ redirect_from: - /developers/overview/secret-scanning-partner-program - /code-security/secret-scanning/secret-scanning-partner-program - /code-security/secret-scanning/secret-scanning-partnership-program/secret-scanning-partner-program + - /code-security/secret-scanning/secret-scanning-partnership-program versions: fpt: '*' ghec: '*' diff --git a/content/contributing/style-guide-and-content-model/about-the-content-model.md b/content/contributing/style-guide-and-content-model/about-the-content-model.md index d07db0d92859..432ef2268fc6 100644 --- a/content/contributing/style-guide-and-content-model/about-the-content-model.md +++ b/content/contributing/style-guide-and-content-model/about-the-content-model.md @@ -43,7 +43,7 @@ If a new top-level doc set is created, it is added to the homepage. If a category serves as the starting point for using a {% data variables.product.prodname_dotcom %} product or feature, it can be added to the homepage. -For example, under the "Security" grouping on the homepage, in addition to the [Code security](/code-security) top-level doc set, the [Supply chain security](/code-security/supply-chain-security), [Security advisories](/code-security/security-advisories), [{% data variables.product.prodname_dependabot %}](/code-security/dependabot), [{% data variables.product.prodname_code_scanning_caps %}](/code-security/code-scanning), and [{% data variables.product.prodname_secret_scanning_caps %}](/code-security/secret-scanning) categories are included because each of those categories are the entry point to {% data variables.product.prodname_dotcom %} products and features. [Security overview](/code-security/security-overview) is not included on the homepage because it provides additional information for using secure coding features and is not an introduction to a product or feature. +For example, under the "Security" grouping on the homepage, in addition to the [Code security](/code-security) top-level doc set, the [Supply chain security](/code-security/supply-chain-security),{% ifversion fpt or ghec %} [Security advisories](/code-security/security-advisories),{% endif %} [{% data variables.product.prodname_dependabot %}](/code-security/dependabot), [{% data variables.product.prodname_code_scanning_caps %}](/code-security/code-scanning), and [{% data variables.product.prodname_secret_scanning_caps %}](/code-security/secret-scanning) categories are included because each of those categories are the entry point to {% data variables.product.prodname_dotcom %} products and features. [Security overview](/code-security/security-overview) is not included on the homepage because it provides additional information for using secure coding features and is not an introduction to a product or feature. ## Top-level doc set diff --git a/content/copilot/concepts/billing/copilot-requests.md b/content/copilot/concepts/billing/copilot-requests.md index c62d6c388a04..1dd22e79ec74 100644 --- a/content/copilot/concepts/billing/copilot-requests.md +++ b/content/copilot/concepts/billing/copilot-requests.md @@ -103,6 +103,6 @@ If you use **{% data variables.copilot.copilot_free_short %}**, you have access Premium request usage is based on the model’s multiplier and the feature you’re using. For example: -* **Using {% data variables.copilot.copilot_claude_opus_41 %} in {% data variables.copilot.copilot_chat_short %}**: With a 10× multiplier, one interaction counts as 10 premium requests. +* **Using {% data variables.copilot.copilot_claude_opus_45 %} in {% data variables.copilot.copilot_chat_short %}**: With a 3× multiplier, one interaction counts as 3 premium requests. * **Using {% data variables.copilot.copilot_gpt_5_mini %} on {% data variables.copilot.copilot_free_short %}**: Each interaction counts as 1 premium request. * **Using {% data variables.copilot.copilot_gpt_5_mini %} on a paid plan**: No premium requests are consumed. diff --git a/content/copilot/reference/ai-models/model-comparison.md b/content/copilot/reference/ai-models/model-comparison.md index 9132e32fe273..f5648ce26314 100644 --- a/content/copilot/reference/ai-models/model-comparison.md +++ b/content/copilot/reference/ai-models/model-comparison.md @@ -40,12 +40,12 @@ Use this table to find a suitable model quickly, see more detail in the sections Use these models for common development tasks that require a balance of quality, speed, and cost efficiency. These models are a good default when you don't have specific requirements. -| Model | Why it's a good fit | -|--------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| {% data variables.copilot.copilot_gpt_5_codex %} | Delivers higher-quality code on complex engineering tasks like features, tests, debugging, refactors, and reviews without lengthy instructions. | -| {% data variables.copilot.copilot_gpt_5_mini %} | Reliable default for most coding and writing tasks. Fast, accurate, and works well across languages and frameworks. | -| {% data variables.copilot.copilot_grok_code %} | Specialized for coding tasks. Performs well on code generation, and debugging across multiple languages. | -| {% data variables.copilot.copilot_raptor_mini %} | Specialized for fast, accurate inline suggestions and explanations. | +| Model | Why it's a good fit | +|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| {% data variables.copilot.copilot_gpt_51_codex %} | Delivers higher-quality code on complex engineering tasks like features, tests, debugging, refactors, and reviews without lengthy instructions. | +| {% data variables.copilot.copilot_gpt_5_mini %} | Reliable default for most coding and writing tasks. Fast, accurate, and works well across languages and frameworks. | +| {% data variables.copilot.copilot_grok_code %} | Specialized for coding tasks. Performs well on code generation, and debugging across multiple languages. | +| {% data variables.copilot.copilot_raptor_mini %} | Specialized for fast, accurate inline suggestions and explanations. | ### When to use these models @@ -92,11 +92,11 @@ These models are designed for tasks that require step-by-step reasoning, complex | Model | Why it's a good fit | |-------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| -| {% data variables.copilot.copilot_gpt_5_mini %} | Delivers deep reasoning and debugging with faster responses and lower resource usage than GPT-5. Ideal for interactive sessions and step-by-step code analysis. | -| {% data variables.copilot.copilot_gpt_5 %} | Great at complex reasoning, code analysis, and technical decision-making. | +| {% data variables.copilot.copilot_gpt_5_mini %} | Delivers deep reasoning and debugging with faster responses and lower resource usage than GPT-5. Ideal for interactive sessions and step-by-step code analysis. | +| {% data variables.copilot.copilot_gpt_52 %} | Great at complex reasoning, code analysis, and technical decision-making. | | {% data variables.copilot.copilot_claude_sonnet_40 %} | Improves on 3.7 with more reliable completions and smarter reasoning under pressure. | -| {% data variables.copilot.copilot_claude_opus_41 %} | Anthropic’s most powerful model. Improves on {% data variables.copilot.copilot_claude_opus %}. | -| {% data variables.copilot.copilot_gemini_25_pro %} | Advanced reasoning across long contexts and scientific or technical analysis. | +| {% data variables.copilot.copilot_claude_opus_45 %} | Anthropic’s most powerful model. Improves on {% data variables.copilot.copilot_claude_opus %}. | +| {% data variables.copilot.copilot_gemini_3_pro %} | Advanced reasoning across long contexts and scientific or technical analysis. | ### When to use these models @@ -117,11 +117,11 @@ For general development workflows or content generation, see [General-purpose co Use these models when you want to ask questions about screenshots, diagrams, UI components, or other visual input. These models support multimodal input and are well suited for front-end work or visual debugging. -| Model | Why it's a good fit | -|-------|---------------------| -| {% data variables.copilot.copilot_gpt_5_mini %} | Reliable default for most coding and writing tasks. Fast, accurate, and supports multimodal input for visual reasoning tasks. Works well across languages and frameworks. | +| Model | Why it's a good fit | +|-------------------------------------------------------|---------------------| +| {% data variables.copilot.copilot_gpt_5_mini %} | Reliable default for most coding and writing tasks. Fast, accurate, and supports multimodal input for visual reasoning tasks. Works well across languages and frameworks. | | {% data variables.copilot.copilot_claude_sonnet_40 %} | Improves on 3.7 with more reliable completions and smarter reasoning under pressure. | -| {% data variables.copilot.copilot_gemini_25_pro %} | Deep reasoning and debugging, ideal for complex code generation, debugging, and research workflows. | +| {% data variables.copilot.copilot_gemini_3_pro %} | Deep reasoning and debugging, ideal for complex code generation, debugging, and research workflows. | ### When to use these models diff --git a/content/copilot/reference/ai-models/supported-models.md b/content/copilot/reference/ai-models/supported-models.md index bb200cd4e228..6a75177ad971 100644 --- a/content/copilot/reference/ai-models/supported-models.md +++ b/content/copilot/reference/ai-models/supported-models.md @@ -51,11 +51,11 @@ This table lists the AI models available in {% data variables.product.prodname_c ## Model retirement history -The following table lists AI models that have been retired from {% data variables.product.prodname_copilot_short %}, along with their retirement dates and suggested alternatives. +The following table lists AI models that are retired or scheduled for retirement from {% data variables.product.prodname_copilot_short %}, along with their retirement dates and suggested alternatives. {% rowheaders %} -| Model name | Retired date | Suggested alternative | +| Model name | Retirement date | Suggested alternative | |-------------------------------------------------------------|-----------------------------|-----------------------------------| | {% for model in tables.copilot.model-deprecation-history %} | | {{ model.name }} | {{ model.retirement_date }} | {{ model.suggested_alternative }} | diff --git a/content/copilot/tutorials/compare-ai-models.md b/content/copilot/tutorials/compare-ai-models.md index dd5566e5a562..a3c127626661 100644 --- a/content/copilot/tutorials/compare-ai-models.md +++ b/content/copilot/tutorials/compare-ai-models.md @@ -126,9 +126,9 @@ print(active_users_sorted) * {% data variables.copilot.copilot_gpt_5_mini %} is optimized for cost and speed, making it ideal for quick edits, prototyping, and utility code. * Use this model when you want reliable answers for simple coding questions without waiting for unnecessary depth. -## {% data variables.copilot.copilot_gpt_5 %} +## {% data variables.copilot.copilot_gpt_52 %} -{% data reusables.copilot.model-use-cases.gpt-5 %} +{% data reusables.copilot.model-use-cases.gpt-52 %} ### Example scenario @@ -172,7 +172,7 @@ class Cart: return Order("", None, 0) ``` -### Why {% data variables.copilot.copilot_gpt_5 %} is a good fit +### Why {% data variables.copilot.copilot_gpt_52 %} is a good fit * It can interpret visual assets, such as UML diagrams, wireframes, or flowcharts, to generate code scaffolding or suggest architecture. * It can be useful for reviewing screenshots of UI layouts or form designs and generating. diff --git a/content/migrations/ado/migrating-repositories-from-azure-devops-to-github-enterprise-cloud.md b/content/migrations/ado/migrating-repositories-from-azure-devops-to-github-enterprise-cloud.md index 02003b38b1da..e11a3c15b9f7 100644 --- a/content/migrations/ado/migrating-repositories-from-azure-devops-to-github-enterprise-cloud.md +++ b/content/migrations/ado/migrating-repositories-from-azure-devops-to-github-enterprise-cloud.md @@ -27,7 +27,7 @@ contentType: other {% endapi %} {% ifversion repo-rules-enterprise %} -{% data reusables.enterprise-migration-tool.deploy-key-bypass %} +{% data reusables.enterprise-migration-tool.repository-migrations-bypass %} {% endif %} ## Prerequisites diff --git a/content/migrations/using-github-enterprise-importer/migrating-between-github-products/about-migrations-between-github-products.md b/content/migrations/using-github-enterprise-importer/migrating-between-github-products/about-migrations-between-github-products.md index 4fec001d16e2..178be86294f3 100644 --- a/content/migrations/using-github-enterprise-importer/migrating-between-github-products/about-migrations-between-github-products.md +++ b/content/migrations/using-github-enterprise-importer/migrating-between-github-products/about-migrations-between-github-products.md @@ -23,7 +23,7 @@ If your migration source is an account on {% data variables.product.prodname_dot The data that {% data variables.product.prodname_importer_proper_name %} migrates depends on the source of the migration and whether you are migrating a repository or organization. {% ifversion repo-rules-enterprise %} -{% data reusables.enterprise-migration-tool.deploy-key-bypass %} +{% data reusables.enterprise-migration-tool.repository-migrations-bypass %} {% endif %} ## Considerations for migrations to {% data variables.product.prodname_ghe_cloud %} diff --git a/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-organizations-from-githubcom-to-github-enterprise-cloud.md b/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-organizations-from-githubcom-to-github-enterprise-cloud.md index e0d67fe57a4f..d649ec4c3df2 100644 --- a/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-organizations-from-githubcom-to-github-enterprise-cloud.md +++ b/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-organizations-from-githubcom-to-github-enterprise-cloud.md @@ -26,6 +26,10 @@ Migrations to {% data variables.product.prodname_ghe_cloud %} include migrations {% data reusables.enterprise-migration-tool.gei-tool-switcher-cli %} {% endapi %} +{% ifversion repo-rules-enterprise %} +{% data reusables.enterprise-migration-tool.repository-migrations-bypass %} +{% endif %} + ## Prerequisites * {% data reusables.enterprise-migration-tool.github-trial-prerequisite %} diff --git a/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-repositories-from-github-enterprise-server-to-github-enterprise-cloud.md b/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-repositories-from-github-enterprise-server-to-github-enterprise-cloud.md index c8723b000a79..3b4aa29920e8 100644 --- a/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-repositories-from-github-enterprise-server-to-github-enterprise-cloud.md +++ b/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-repositories-from-github-enterprise-server-to-github-enterprise-cloud.md @@ -47,6 +47,10 @@ To migrate your repositories from {% data variables.product.prodname_ghe_server {% endapi %} +{% ifversion repo-rules-enterprise %} +{% data reusables.enterprise-migration-tool.repository-migrations-bypass %} +{% endif %} + ## Prerequisites * {% data reusables.enterprise-migration-tool.github-trial-prerequisite %} diff --git a/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-repositories-from-githubcom-to-github-enterprise-cloud.md b/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-repositories-from-githubcom-to-github-enterprise-cloud.md index 5eea87d3c797..fad9d61e2f04 100644 --- a/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-repositories-from-githubcom-to-github-enterprise-cloud.md +++ b/content/migrations/using-github-enterprise-importer/migrating-between-github-products/migrating-repositories-from-githubcom-to-github-enterprise-cloud.md @@ -27,7 +27,7 @@ Migrations to {% data variables.product.prodname_ghe_cloud %} include migrations {% endapi %} {% ifversion repo-rules-enterprise %} -{% data reusables.enterprise-migration-tool.deploy-key-bypass %} +{% data reusables.enterprise-migration-tool.repository-migrations-bypass %} {% endif %} ## Prerequisites diff --git a/content/migrations/using-github-enterprise-importer/migrating-from-bitbucket-server-to-github-enterprise-cloud/migrating-repositories-from-bitbucket-server-to-github-enterprise-cloud.md b/content/migrations/using-github-enterprise-importer/migrating-from-bitbucket-server-to-github-enterprise-cloud/migrating-repositories-from-bitbucket-server-to-github-enterprise-cloud.md index e0c99a0a22eb..0457d6e901c0 100644 --- a/content/migrations/using-github-enterprise-importer/migrating-from-bitbucket-server-to-github-enterprise-cloud/migrating-repositories-from-bitbucket-server-to-github-enterprise-cloud.md +++ b/content/migrations/using-github-enterprise-importer/migrating-from-bitbucket-server-to-github-enterprise-cloud/migrating-repositories-from-bitbucket-server-to-github-enterprise-cloud.md @@ -19,7 +19,7 @@ You can migrate individual repositories or all repositories from a BitBucket Ser At this time, migrating from Bitbucket Server with the {% data variables.product.prodname_dotcom %} API is not supported. {% ifversion repo-rules-enterprise %} -{% data reusables.enterprise-migration-tool.deploy-key-bypass %} +{% data reusables.enterprise-migration-tool.repository-migrations-bypass %} {% endif %} ## Prerequisites diff --git a/data/release-notes/enterprise-server/3-19/1.yml b/data/release-notes/enterprise-server/3-19/1.yml index c4509f159c4f..b706c75a215d 100644 --- a/data/release-notes/enterprise-server/3-19/1.yml +++ b/data/release-notes/enterprise-server/3-19/1.yml @@ -87,3 +87,5 @@ sections: When publishing npm packages in a workflow after restoring from a backup to GitHub Enterprise Server 3.13.5.gm4 or 3.14.2.gm3, you may encounter a `401 Unauthorized` error from the GitHub Packages service. This can happen if the restore is from an N-1 or N-2 version and the workflow targets the npm endpoint on the backup instance. To avoid this issue, ensure the access token is valid and includes the correct scopes for publishing to GitHub Packages. - | The setting to define private registries at the organization level for code scanning is only available if Dependabot is enabled for the instance. + - | + In patch 3.19.1, we identified an issue in the Management Console where the Backups (Preview) and Updates tabs may fail to open and instead return an Internal Server Error. We recommend using the command line interface (CLI) for backups and updates until an updated patch is released. [Updated: 2026-01-13] diff --git a/data/reusables/copilot/model-use-cases/gpt-5.md b/data/reusables/copilot/model-use-cases/gpt-5.md deleted file mode 100644 index 7a59405da965..000000000000 --- a/data/reusables/copilot/model-use-cases/gpt-5.md +++ /dev/null @@ -1 +0,0 @@ - {% data variables.copilot.copilot_gpt_5 %} supports image input so that developers can bring visual context into tasks like UI inspection, diagram analysis, or layout debugging. This makes {% data variables.copilot.copilot_gpt_5 %} particularly useful for scenarios where image-based input enhances problem-solving, such as asking {% data variables.product.prodname_copilot_short %} to analyze a UI screenshot for accessibility issues or to help understand a visual bug in a layout. diff --git a/data/reusables/copilot/model-use-cases/gpt-52.md b/data/reusables/copilot/model-use-cases/gpt-52.md new file mode 100644 index 000000000000..4034110a9bdd --- /dev/null +++ b/data/reusables/copilot/model-use-cases/gpt-52.md @@ -0,0 +1 @@ + {% data variables.copilot.copilot_gpt_52 %} supports image input so that developers can bring visual context into tasks like UI inspection, diagram analysis, or layout debugging. This makes {% data variables.copilot.copilot_gpt_52 %} particularly useful for scenarios where image-based input enhances problem-solving, such as asking {% data variables.product.prodname_copilot_short %} to analyze a UI screenshot for accessibility issues or to help understand a visual bug in a layout. diff --git a/data/reusables/enterprise-migration-tool/deploy-key-bypass.md b/data/reusables/enterprise-migration-tool/deploy-key-bypass.md index 6d7e8d5b20f8..8786c6c8c50f 100644 --- a/data/reusables/enterprise-migration-tool/deploy-key-bypass.md +++ b/data/reusables/enterprise-migration-tool/deploy-key-bypass.md @@ -1,3 +1,4 @@ > [!NOTE] If the repository you are migrating has rulesets that the incoming repository doesn't match, the migration will be blocked. To bypass these rulesets and allow the migration, you can apply a ruleset bypass for all deploy keys in the target organization. > > Repository rulesets can be set at the organization level. If the incoming repository does not match any of these rulesets, you will need to use the deploy key bypass for each one. See [AUTOTITLE](/organizations/managing-organization-settings/creating-rulesets-for-repositories-in-your-organization#granting-bypass-permissions-for-your-branch-or-tag-ruleset). +> diff --git a/data/reusables/enterprise-migration-tool/repository-migrations-bypass.md b/data/reusables/enterprise-migration-tool/repository-migrations-bypass.md new file mode 100644 index 000000000000..396da2dc8647 --- /dev/null +++ b/data/reusables/enterprise-migration-tool/repository-migrations-bypass.md @@ -0,0 +1,9 @@ +If the destination organization or enterprise of your migration has rulesets enabled, the migrated repository's history may violate those rules. To allow the migration without disabling your rulesets, add "Repository migrations" to the bypass list for each applicable ruleset. This bypass applies only during the migration. Once complete, rulesets will be enforced on all new contributions. + +To configure the bypass: + +1. Navigate to each enterprise or organization ruleset. +1. In the "Bypass list" section, click **Add bypass**. +1. Select **Repository migrations**. + +For more information, see [AUTOTITLE](/organizations/managing-organization-settings/creating-rulesets-for-repositories-in-your-organization#granting-bypass-permissions-for-your-branch-or-tag-ruleset). diff --git a/data/tables/copilot/model-deprecation-history.yml b/data/tables/copilot/model-deprecation-history.yml index b97d7b5d9887..70994d07e389 100644 --- a/data/tables/copilot/model-deprecation-history.yml +++ b/data/tables/copilot/model-deprecation-history.yml @@ -11,6 +11,22 @@ # - retirement_date: The official retirement date for the model (YYYY-MM-DD). # - suggested_alternative: The model recommended for migration. +- name: 'Claude Opus 4.1' + retirement_date: '2026-02-17' + suggested_alternative: 'Claude Opus 4.5' + +- name: 'Gemini 2.5 Pro' + retirement_date: '2026-02-17' + suggested_alternative: 'Gemini 3 Pro' + +- name: 'GPT-5' + retirement_date: '2026-02-17' + suggested_alternative: 'GPT-5.2' + +- name: 'GPT-5-Codex' + retirement_date: '2026-02-17' + suggested_alternative: 'GPT-5.1-Codex' + - name: 'Claude Sonnet 3.5' retirement_date: '2025-11-06' suggested_alternative: 'Claude Haiku 4.5' diff --git a/data/tables/copilot/model-release-status.yml b/data/tables/copilot/model-release-status.yml index 644f31d6d618..06879b9bc539 100644 --- a/data/tables/copilot/model-release-status.yml +++ b/data/tables/copilot/model-release-status.yml @@ -27,7 +27,7 @@ - name: 'GPT-5' provider: 'OpenAI' - release_status: 'GA' + release_status: 'Closing down: 2026-02-17' agent_mode: true ask_mode: true edit_mode: true @@ -41,7 +41,7 @@ - name: 'GPT-5-Codex' provider: 'OpenAI' - release_status: 'Public preview' + release_status: 'Closing down: 2026-02-17' agent_mode: true ask_mode: true edit_mode: true @@ -91,7 +91,7 @@ - name: 'Claude Opus 4.1' provider: 'Anthropic' - release_status: 'GA' + release_status: 'Closing down: 2026-02-17' agent_mode: false ask_mode: true edit_mode: true @@ -121,7 +121,7 @@ - name: 'Gemini 2.5 Pro' provider: 'Google' - release_status: 'GA' + release_status: 'Closing down: 2026-02-17' agent_mode: true ask_mode: true edit_mode: true diff --git a/src/article-api/lib/graphql-helpers.ts b/src/article-api/lib/graphql-helpers.ts new file mode 100644 index 000000000000..6db482d7ff06 --- /dev/null +++ b/src/article-api/lib/graphql-helpers.ts @@ -0,0 +1,32 @@ +import type { Context, Page } from '@/types' +import { renderContent } from '@/content-render/index' +import matter from '@gr2m/gray-matter' + +/** + * Extract manual content from page markdown + * Used by GraphQL transformers to get content before the auto-generated marker + */ +export async function extractManualContent(page: Page, context: Context): Promise { + if (!page.markdown) return '' + + const markerIndex = page.markdown.indexOf( + '', + ) + + if (markerIndex <= 0) return '' + + const { content } = matter(page.markdown) + const manualContentMarkerIndex = content.indexOf( + '', + ) + + if (manualContentMarkerIndex <= 0) return '' + + const rawManualContent = content.substring(0, manualContentMarkerIndex).trim() + if (!rawManualContent) return '' + + return await renderContent(rawManualContent, { + ...context, + markdownRequested: true, + }) +} diff --git a/src/article-api/middleware/article-body.ts b/src/article-api/middleware/article-body.ts index 31dd93626ed6..c6b82b59f627 100644 --- a/src/article-api/middleware/article-body.ts +++ b/src/article-api/middleware/article-body.ts @@ -74,5 +74,20 @@ export async function getArticleBody(req: ExtendedRequestWithPageInfo) { // these parts allow us to render the page const renderingReq = await createContextualizedRenderingRequest(pathname, page) renderingReq.context.markdownRequested = true - return await page.render(renderingReq.context) + const content = await page.render(renderingReq.context) + + // Get title and intro for consistency with transformer-based pages + const title = page.title + const intro = page.intro + ? await page.renderProp('intro', renderingReq.context, { textOnly: true }) + : '' + + // Prepend title and intro to the content + let result = `# ${title}\n\n` + if (intro) { + result += `${intro}\n\n` + } + result += content + + return result } diff --git a/src/article-api/templates/codeql-cli-page.template.md b/src/article-api/templates/codeql-cli-page.template.md new file mode 100644 index 000000000000..a497a9dde462 --- /dev/null +++ b/src/article-api/templates/codeql-cli-page.template.md @@ -0,0 +1,7 @@ +# {{ page.title }} + +{% if page.intro %} +{{ page.intro }} +{% endif %} + +{{ content }} diff --git a/src/article-api/templates/secret-scanning-page.template.md b/src/article-api/templates/secret-scanning-page.template.md new file mode 100644 index 000000000000..a497a9dde462 --- /dev/null +++ b/src/article-api/templates/secret-scanning-page.template.md @@ -0,0 +1,7 @@ +# {{ page.title }} + +{% if page.intro %} +{{ page.intro }} +{% endif %} + +{{ content }} diff --git a/src/article-api/templates/webhooks-page.template.md b/src/article-api/templates/webhooks-page.template.md index c7a4401d0791..dbf51b71d30f 100644 --- a/src/article-api/templates/webhooks-page.template.md +++ b/src/article-api/templates/webhooks-page.template.md @@ -11,12 +11,42 @@ {% for webhook in webhooks %} ## {{ webhook.name }} -**Available actions:** {% for actionType in webhook.actionTypes %}{% if forloop.last and forloop.length > 1 %}and {% endif %}`{{ actionType }}`{% unless forloop.last %}{% if forloop.length > 2 %}, {% else %} {% endif %}{% endunless %}{% endfor %} +{% if webhook.summary %} +{{ webhook.summary }} -{% if webhook.data.descriptionHtml %} -{{ webhook.data.descriptionHtml }} {% endif %} +{% if webhook.availability.size > 0 %} +### Availability -**Availability:** {% for availability in webhook.data.availability %}{% if forloop.last and forloop.length > 1 %}and {% endif %}`{{ availability }}`{% unless forloop.last %}{% if forloop.length > 2 %}, {% else %} {% endif %}{% endunless %}{% endfor %} +{% for avail in webhook.availability %}- `{{ avail }}` +{% endfor %} + +{% endif %} +### Webhook payload object + +{% if webhook.actionTypes.size > 1 %} +**Action type:** {% for actionType in webhook.actionTypes %}`{{ actionType }}`{% unless forloop.last %}, {% endunless %}{% endfor %} + +{% endif %} +{% if webhook.description %} +{{ webhook.description }} + +{% endif %} +{% if webhook.bodyParameters.size > 0 %} +#### Webhook payload object parameters + +| Name | Type | Description | +|------|------|-------------| +{% for param in webhook.bodyParameters %}| `{{ param.name }}` | `{{ param.type }}` | {% if param.isRequired %}**Required.** {% endif %}{{ param.description }} | +{% endfor %} +{% endif %} +{% if webhook.payloadExample %} +### Webhook payload example + +```json +{{ webhook.payloadExample }} +``` + +{% endif %} {% endfor %} diff --git a/src/article-api/tests/article-body.ts b/src/article-api/tests/article-body.ts index 5d2814cfa41b..26eb956da5d0 100644 --- a/src/article-api/tests/article-body.ts +++ b/src/article-api/tests/article-body.ts @@ -25,6 +25,15 @@ describe('article body api', () => { expect(res.headers['content-type']).toContain('text/markdown') }) + test('body includes title and intro', async () => { + const res = await get(makeURL('/en/get-started/start-your-journey/hello-world')) + expect(res.statusCode).toBe(200) + // Body should start with the page title as H1 + expect(res.body).toMatch(/^# Hello World/) + // Body should include the intro after the title + expect(res.body).toContain('Follow this Hello World exercise to get started with') + }) + test('octicons auto-generate aria-labels', async () => { const res = await get(makeURL('/en/get-started/start-your-journey/hello-world')) expect(res.statusCode).toBe(200) diff --git a/src/article-api/tests/bespoke-landing-transformer.ts b/src/article-api/tests/bespoke-landing-transformer.ts new file mode 100644 index 000000000000..0e6828ea5c0a --- /dev/null +++ b/src/article-api/tests/bespoke-landing-transformer.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from 'vitest' + +import { get } from '@/tests/helpers/e2etest' + +const makeURL = (pathname: string): string => + `/api/article/body?${new URLSearchParams({ pathname })}` + +describe('bespoke landing transformer', () => { + test('renders a bespoke landing page with all sections', async () => { + // /en/get-started/article-grid-bespoke is a bespoke landing page + const res = await get(makeURL('/en/get-started/article-grid-bespoke')) + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toContain('text/markdown') + + // Check for title + expect(res.body).toContain('# Article Grid Bespoke Landing') + + // Should have intro + expect(res.body).toContain('A test page for testing') + }) + + test('renders all descendant articles recursively', async () => { + const res = await get(makeURL('/en/get-started/article-grid-bespoke')) + expect(res.statusCode).toBe(200) + + // Should have Articles section with all descendant articles (recursive) + expect(res.body).toContain('## Articles') + expect(res.body).toContain('[Grid Article One]') + expect(res.body).toContain('[Grid Article Two]') + expect(res.body).toContain('[Grid Article Three]') + expect(res.body).toContain('[Grid Article Four]') + }) +}) diff --git a/src/article-api/tests/journey-landing-transformer.ts b/src/article-api/tests/journey-landing-transformer.ts new file mode 100644 index 000000000000..09e56d9af8fc --- /dev/null +++ b/src/article-api/tests/journey-landing-transformer.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from 'vitest' + +import { get } from '@/tests/helpers/e2etest' + +const makeURL = (pathname: string): string => + `/api/article/body?${new URLSearchParams({ pathname })}` + +describe('journey landing transformer', () => { + test('renders a journey landing page in markdown', async () => { + // /en/get-started/test-journey is a journey landing page in the fixtures + const res = await get(makeURL('/en/get-started/test-journey')) + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toContain('text/markdown') + + // Check for journey tracks (now under Links section with track title as h3) + expect(res.body).toContain('## Links') + expect(res.body).toContain('### First Track') + expect(res.body).toContain('* [Hello World](/en/get-started/start-your-journey/hello-world)') + }) +}) diff --git a/src/article-api/tests/webhooks-transformer.ts b/src/article-api/tests/webhooks-transformer.ts index 630b45ebfba8..4bf18d7ef21f 100644 --- a/src/article-api/tests/webhooks-transformer.ts +++ b/src/article-api/tests/webhooks-transformer.ts @@ -93,8 +93,8 @@ describe('Webhooks transformer', () => { // Should show payload object parameters section expect(res.body).toContain('### Webhook payload object') expect(res.body).toContain('#### Webhook payload object parameters') - // Should have a markdown table with parameter columns - expect(res.body).toContain('| Name | Type | Description |') + // Should have a markdown table with parameter columns (may have extra spacing from formatting) + expect(res.body).toMatch(/\|\s*Name\s*\|\s*Type\s*\|\s*Description\s*\|/) }) test('webhooks show descriptions', async () => { diff --git a/src/article-api/transformers/audit-logs-transformer.ts b/src/article-api/transformers/audit-logs-transformer.ts index 962e47c3d884..1da904f694d8 100644 --- a/src/article-api/transformers/audit-logs-transformer.ts +++ b/src/article-api/transformers/audit-logs-transformer.ts @@ -2,19 +2,16 @@ import type { Context, Page } from '@/types' import type { PageTransformer } from './types' import type { CategorizedEvents } from '@/audit-logs/types' import { renderContent } from '@/content-render/index' +import { loadTemplate } from '@/article-api/lib/load-template' import matter from '@gr2m/gray-matter' -import { readFileSync } from 'fs' -import { join, dirname } from 'path' -import { fileURLToPath } from 'url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) /** * Transformer for Audit Logs pages * Converts audit log events and their data into markdown format using a Liquid template */ export class AuditLogsTransformer implements PageTransformer { + templateName = 'audit-logs-page.template.md' + canTransform(page: Page): boolean { return page.autogenerated === 'audit-logs' } @@ -76,8 +73,7 @@ export class AuditLogsTransformer implements PageTransformer { ) // Load and render template - const templatePath = join(__dirname, '../templates/audit-logs-page.template.md') - const templateContent = readFileSync(templatePath, 'utf8') + const templateContent = loadTemplate(this.templateName) // Render the template with Liquid const rendered = await renderContent(templateContent, { diff --git a/src/article-api/transformers/bespoke-landing-transformer.ts b/src/article-api/transformers/bespoke-landing-transformer.ts new file mode 100644 index 000000000000..e3c52ed3a35e --- /dev/null +++ b/src/article-api/transformers/bespoke-landing-transformer.ts @@ -0,0 +1,121 @@ +import type { Context, Page } from '@/types' +import type { PageTransformer, TemplateData, Section, LinkData } from './types' +import { renderContent } from '@/content-render/index' +import { loadTemplate } from '@/article-api/lib/load-template' +import { getAllTocItems, flattenTocItems } from '@/article-api/lib/get-all-toc-items' + +interface RecommendedItem { + href: string + title?: string + intro?: string +} + +interface BespokeLandingPage extends Omit { + featuredLinks?: Record> + children?: string[] + recommended?: RecommendedItem[] + rawRecommended?: string[] + includedCategories?: string[] +} + +/** + * Transforms bespoke-landing pages into markdown format. + * Handles recommended carousel and full article listings. + * Note: Unlike discovery-landing, bespoke-landing shows ALL articles + * regardless of includedCategories. + */ +export class BespokeLandingTransformer implements PageTransformer { + templateName = 'landing-page.template.md' + + canTransform(page: Page): boolean { + return page.layout === 'bespoke-landing' + } + + async transform(page: Page, pathname: string, context: Context): Promise { + const templateData = await this.prepareTemplateData(page, pathname, context) + + const templateContent = loadTemplate(this.templateName) + + const rendered = await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) + + return rendered + } + + private async prepareTemplateData( + page: Page, + _pathname: string, + context: Context, + ): Promise { + const bespokePage = page as BespokeLandingPage + const sections: Section[] = [] + + // Recommended carousel + const recommended = bespokePage.recommended ?? bespokePage.rawRecommended + if (recommended && recommended.length > 0) { + const { default: getLearningTrackLinkData } = await import( + '@/learning-track/lib/get-link-data' + ) + + let links: LinkData[] + if (typeof recommended[0] === 'object' && 'title' in recommended[0]) { + links = recommended.map((item) => ({ + href: typeof item === 'string' ? item : item.href, + title: (typeof item === 'object' && item.title) || '', + intro: (typeof item === 'object' && item.intro) || '', + })) + } else { + const linkData = await getLearningTrackLinkData(recommended as string[], context, { + title: true, + intro: true, + }) + links = (linkData || []).map((item: { href: string; title?: string; intro?: string }) => ({ + href: item.href, + title: item.title || '', + intro: item.intro || '', + })) + } + + const validLinks = links.filter((l) => l.href && l.title) + if (validLinks.length > 0) { + sections.push({ + title: 'Recommended', + groups: [{ title: null, links: validLinks }], + }) + } + } + + // Articles section: recursively gather ALL descendant articles + // This matches the behavior of the site which uses genericTocFlat/genericTocNested + // Note: For bespoke-landing pages, the site shows ALL articles regardless of includedCategories + // (includedCategories only filters for discovery-landing pages) + if (bespokePage.children && bespokePage.children.length > 0) { + const tocItems = await getAllTocItems(page, context, { + recurse: true, + renderIntros: true, + }) + + // Flatten to get all leaf articles (excludeParents: true means only get articles, not category pages) + const allArticles = flattenTocItems(tocItems, { excludeParents: true }) + + if (allArticles.length > 0) { + sections.push({ + title: 'Articles', + groups: [{ title: null, links: allArticles }], + }) + } + } + + 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/codeql-cli-transformer.ts b/src/article-api/transformers/codeql-cli-transformer.ts index 9aefa8133845..f32c8fbc903e 100644 --- a/src/article-api/transformers/codeql-cli-transformer.ts +++ b/src/article-api/transformers/codeql-cli-transformer.ts @@ -1,28 +1,47 @@ import type { Context, Page } from '@/types' import type { PageTransformer } from './types' +import { renderContent } from '@/content-render/index' +import { loadTemplate } from '@/article-api/lib/load-template' import { stripHtmlCommentsAndNormalizeWhitespace } from '@/article-api/lib/strip-html-comments' /** * Transformer for CodeQL CLI reference pages. - * Renders autogenerated CodeQL CLI documentation pages as markdown. + * Renders autogenerated CodeQL CLI documentation pages as markdown using a Liquid template. * Sets `markdownRequested` to true in the context to ensure the page is rendered as markdown, * bypassing the default article type check. */ export class CodeQLCliTransformer implements PageTransformer { + templateName = 'codeql-cli-page.template.md' + canTransform(page: Page): boolean { return page.autogenerated === 'codeql-cli' } async transform(page: Page, _pathname: string, context: Context): Promise { // CodeQL CLI pages are fully generated markdown files in the repo. - // We render them with markdownRequested=true to get the markdown output, - // similar to how regular articles are rendered but through the transformer pattern. + // We render them with markdownRequested=true to get the markdown output. context.markdownRequested = true const content = await page.render(context) const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' - const result = `# ${page.title}\n\n${intro}\n\n${content}` + // Prepare template data + const templateData: Record = { + page: { + title: page.title, + intro, + }, + content, + } + + // Load and render template + const templateContent = loadTemplate(this.templateName) + + const result = await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) // Strip HTML comments (e.g., markdownlint-disable comments) from the output return stripHtmlCommentsAndNormalizeWhitespace(result) diff --git a/src/article-api/transformers/github-apps-transformer.ts b/src/article-api/transformers/github-apps-transformer.ts index 522c9873b50d..f04b9b1e3c94 100644 --- a/src/article-api/transformers/github-apps-transformer.ts +++ b/src/article-api/transformers/github-apps-transformer.ts @@ -1,13 +1,9 @@ import type { Context, Page } from '@/types' import type { PageTransformer } from './types' import { renderContent } from '@/content-render/index' +import { loadTemplate } from '@/article-api/lib/load-template' import matter from '@gr2m/gray-matter' -import { readFileSync } from 'fs' -import { join, dirname } from 'path' -import { fileURLToPath } from 'url' -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) const DEBUG = process.env.RUNNER_DEBUG === '1' || process.env.DEBUG === '1' // GitHub Apps data types @@ -91,6 +87,8 @@ const PERMISSIONS_PAGE_TYPES = new Set([ * in TypeScript for permissions pages to avoid Liquid escaping issues. */ export class GithubAppsTransformer implements PageTransformer { + templateName = 'github-apps-page.template.md' + canTransform(page: Page): boolean { return page.autogenerated === 'github-apps' } @@ -160,9 +158,8 @@ export class GithubAppsTransformer implements PageTransformer { isPermissionsPage, ) - // Load and render template - const templatePath = join(__dirname, '../templates/github-apps-page.template.md') - const templateContent = readFileSync(templatePath, 'utf8') + // Load template + const templateContent = loadTemplate(this.templateName) // For permissions pages, we need to construct the tables manually to avoid Liquid escaping let finalContent: string diff --git a/src/article-api/transformers/graphql-breaking-changes-transformer.ts b/src/article-api/transformers/graphql-breaking-changes-transformer.ts new file mode 100644 index 000000000000..e0268c7351c5 --- /dev/null +++ b/src/article-api/transformers/graphql-breaking-changes-transformer.ts @@ -0,0 +1,69 @@ +import type { Context, Page } from '@/types' +import type { PageTransformer } from './types' +import type { BreakingChangesT } from '@/graphql/components/types' +import { renderContent } from '@/content-render/index' +import { loadTemplate } from '@/article-api/lib/load-template' +import { fastTextOnly } from '@/content-render/unified/text-only' +import { extractManualContent } from '@/article-api/lib/graphql-helpers' +import GithubSlugger from 'github-slugger' + +/** + * Transformer for GraphQL breaking changes page + * Renders breaking changes organized by date + */ +export class GraphQLBreakingChangesTransformer implements PageTransformer { + templateName = 'graphql-breaking-changes.template.md' + + canTransform(page: Page): boolean { + if (page.autogenerated !== 'graphql') return false + + return page.relativePath.includes('graphql/overview/breaking-changes') + } + + async transform(page: Page, _pathname: string, context: Context): Promise { + const currentVersion = context.currentVersion! + + const { getGraphqlBreakingChanges } = await import('@/graphql/lib/index') + + const schema = getGraphqlBreakingChanges(currentVersion) as BreakingChangesT + + const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' + const manualContent = await extractManualContent(page, context) + + const slugger = new GithubSlugger() + + // Process breaking changes by date + const breakingChangesByDate = Object.keys(schema).map((date) => { + const items = schema[date] + const heading = `Changes scheduled for ${date}` + const slug = slugger.slug(heading) + + return { + date, + heading, + slug, + items: items.map((item) => ({ + location: item.location, + description: fastTextOnly(item.description), + reason: fastTextOnly(item.reason), + criticality: item.criticality, + })), + } + }) + + const templateData: Record = { + pageTitle: page.title, + pageIntro: intro, + manualContent, + breakingChangesByDate, + } + + const templateContent = loadTemplate(this.templateName) + + return await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) + } +} diff --git a/src/article-api/transformers/graphql-changelog-transformer.ts b/src/article-api/transformers/graphql-changelog-transformer.ts new file mode 100644 index 000000000000..0e3b504cbce0 --- /dev/null +++ b/src/article-api/transformers/graphql-changelog-transformer.ts @@ -0,0 +1,69 @@ +import type { Context, Page } from '@/types' +import type { PageTransformer } from './types' +import type { ChangelogItemT } from '@/graphql/components/types' +import { renderContent } from '@/content-render/index' +import { loadTemplate } from '@/article-api/lib/load-template' +import { fastTextOnly } from '@/content-render/unified/text-only' +import { extractManualContent } from '@/article-api/lib/graphql-helpers' + +/** + * Transformer for GraphQL changelog page + * Renders the changelog with schema changes, preview changes, and upcoming changes + */ +export class GraphQLChangelogTransformer implements PageTransformer { + templateName = 'graphql-changelog.template.md' + + canTransform(page: Page): boolean { + if (page.autogenerated !== 'graphql') return false + + return page.relativePath.includes('graphql/overview/changelog') + } + + async transform(page: Page, _pathname: string, context: Context): Promise { + const currentVersion = context.currentVersion! + + const { getGraphqlChangelog } = await import('@/graphql/lib/index') + + const schema = getGraphqlChangelog(currentVersion) as ChangelogItemT[] + + const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' + const manualContent = await extractManualContent(page, context) + + // Process changelog items + const changelogItems = schema.map((item) => { + const processChanges = (changes: Array<{ title: string; changes: string[] }>) => + changes.map((change) => ({ + title: change.title, + changes: change.changes.map((html: string) => { + // Remove wrapping

tags if present + if (html.startsWith('

') && html.endsWith('

')) { + return fastTextOnly(html.slice(3, -4)) + } + return fastTextOnly(html) + }), + })) + + return { + date: item.date, + schemaChanges: processChanges(item.schemaChanges || []), + previewChanges: processChanges(item.previewChanges || []), + upcomingChanges: processChanges(item.upcomingChanges || []), + } + }) + + const templateData: Record = { + pageTitle: page.title, + pageIntro: intro, + manualContent, + changelogItems, + } + + const templateContent = loadTemplate(this.templateName) + + return await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) + } +} diff --git a/src/article-api/transformers/graphql-index-transformer.ts b/src/article-api/transformers/graphql-index-transformer.ts new file mode 100644 index 000000000000..b46b41cf9100 --- /dev/null +++ b/src/article-api/transformers/graphql-index-transformer.ts @@ -0,0 +1,55 @@ +import type { Context, Page } from '@/types' +import type { PageTransformer } from './types' +import { renderContent } from '@/content-render/index' +import { loadTemplate } from '@/article-api/lib/load-template' +import { extractManualContent } from '@/article-api/lib/graphql-helpers' + +/** + * Transformer for GraphQL reference index page + * Renders the index page with links to child pages + */ +export class GraphQLIndexTransformer implements PageTransformer { + templateName = 'graphql-index.template.md' + + canTransform(page: Page): boolean { + if (page.autogenerated !== 'graphql') return false + + // Match the reference index page (no specific page type after /reference) + return page.relativePath.endsWith('graphql/reference/index.md') + } + + async transform(page: Page, _pathname: string, context: Context): Promise { + const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' + + const manualContent = await extractManualContent(page, context) + + // Get children links from page metadata + const children = page.children || [] + const childrenLinks = children + .map((child) => { + const childPath = child.startsWith('/') ? child : `/${child}` + const childName = childPath.split('/').pop() || '' + const displayName = childName + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + return `- [${displayName}](${childPath})` + }) + .join('\n') + + const templateData: Record = { + pageTitle: page.title, + pageIntro: intro, + manualContent, + childrenLinks, + } + + const templateContent = loadTemplate(this.templateName) + + return await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) + } +} diff --git a/src/article-api/transformers/graphql-transformer.ts b/src/article-api/transformers/graphql-reference-transformer.ts similarity index 53% rename from src/article-api/transformers/graphql-transformer.ts rename to src/article-api/transformers/graphql-reference-transformer.ts index 12a2c43a049d..418ad9ef5ff9 100644 --- a/src/article-api/transformers/graphql-transformer.ts +++ b/src/article-api/transformers/graphql-reference-transformer.ts @@ -9,28 +9,28 @@ import type { UnionT, InputObjectT, ScalarT, - ChangelogItemT, - BreakingChangesT, FieldT, } from '@/graphql/components/types' import { renderContent } from '@/content-render/index' -import matter from '@gr2m/gray-matter' -import { readFileSync } from 'fs' -import { join, dirname } from 'path' -import { fileURLToPath } from 'url' +import { loadTemplate } from '@/article-api/lib/load-template' import { fastTextOnly } from '@/content-render/unified/text-only' -import GithubSlugger from 'github-slugger' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) +import { extractManualContent } from '@/article-api/lib/graphql-helpers' /** - * Transformer for GraphQL pages - * Converts GraphQL schema data into markdown format using Liquid templates + * Transformer for GraphQL reference pages (queries, mutations, objects, etc.) + * Renders schema items with their fields and arguments */ -export class GraphQLTransformer implements PageTransformer { +export class GraphQLReferenceTransformer implements PageTransformer { + templateName = 'graphql-reference.template.md' + canTransform(page: Page): boolean { - return page.autogenerated === 'graphql' + if (page.autogenerated !== 'graphql') return false + + // Match reference pages that have a specific page type (not index) + const isReference = page.relativePath.includes('graphql/reference/') + const isNotIndex = !page.relativePath.endsWith('index.md') + + return isReference && isNotIndex } async transform(page: Page, pathname: string, context: Context): Promise { @@ -39,77 +39,8 @@ export class GraphQLTransformer implements PageTransformer { // Determine the page type from the pathname const pathParts = pathname.split('/').filter(Boolean) const graphqlIndex = pathParts.indexOf('graphql') + const pageType = pathParts[graphqlIndex + 2] // specific page like 'queries', 'mutations', etc. - if (graphqlIndex === -1) { - throw new Error(`Invalid GraphQL path: ${pathname}`) - } - - const section = pathParts[graphqlIndex + 1] // 'reference' or 'overview' - const pageType = pathParts[graphqlIndex + 2] // specific page like 'queries', 'changelog', etc. - - // Handle different GraphQL page types - if (section === 'overview' && pageType === 'changelog') { - return await this.transformChangelog(page, currentVersion, context) - } else if (section === 'overview' && pageType === 'breaking-changes') { - return await this.transformBreakingChanges(page, currentVersion, context) - } else if (section === 'reference' && pageType) { - return await this.transformReference(page, currentVersion, context, pageType) - } else if (section === 'reference' && !pageType) { - // Index page - just render the intro and manual content - return await this.transformIndexPage(page, context) - } - - throw new Error(`Unsupported GraphQL page type: ${pathname}`) - } - - /** - * Transform the GraphQL reference index page - */ - private async transformIndexPage(page: Page, context: Context): Promise { - const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' - - const manualContent = await this.extractManualContent(page, context) - - // Get children links from page metadata - const children = page.children || [] - const childrenLinks = children - .map((child) => { - const childPath = child.startsWith('/') ? child : `/${child}` - const childName = childPath.split('/').pop() || '' - const displayName = childName - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') - return `- [${displayName}](${childPath})` - }) - .join('\n') - - const templateData = { - pageTitle: page.title, - pageIntro: intro, - manualContent, - childrenLinks, - } - - const templatePath = join(__dirname, '../templates/graphql-index.template.md') - const templateContent = readFileSync(templatePath, 'utf8') - - return await renderContent(templateContent, { - ...context, - ...templateData, - markdownRequested: true, - }) - } - - /** - * Transform GraphQL reference pages (queries, mutations, objects, etc.) - */ - private async transformReference( - page: Page, - currentVersion: string, - context: Context, - pageType: string, - ): Promise { // Import GraphQL data functions dynamically const { getGraphqlSchema } = await import('@/graphql/lib/index') @@ -120,7 +51,7 @@ export class GraphQLTransformer implements PageTransformer { // Prepare intro and manual content const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' - const manualContent = await this.extractManualContent(page, context) + const manualContent = await extractManualContent(page, context) // Prepare the schema items based on page type let preparedItems: Array> = [] @@ -166,7 +97,7 @@ export class GraphQLTransformer implements PageTransformer { break } - const templateData = { + const templateData: Record = { pageTitle: page.title, pageIntro: intro, manualContent, @@ -174,62 +105,7 @@ export class GraphQLTransformer implements PageTransformer { pageType: schemaKey, } - const templatePath = join(__dirname, '../templates/graphql-reference.template.md') - const templateContent = readFileSync(templatePath, 'utf8') - - return await renderContent(templateContent, { - ...context, - ...templateData, - markdownRequested: true, - }) - } - - /** - * Transform changelog page - */ - private async transformChangelog( - page: Page, - currentVersion: string, - context: Context, - ): Promise { - const { getGraphqlChangelog } = await import('@/graphql/lib/index') - - const schema = getGraphqlChangelog(currentVersion) as ChangelogItemT[] - - const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' - const manualContent = await this.extractManualContent(page, context) - - // Process changelog items - const changelogItems = schema.map((item) => { - const processChanges = (changes: Array<{ title: string; changes: string[] }>) => - changes.map((change) => ({ - title: change.title, - changes: change.changes.map((html: string) => { - // Remove wrapping

tags if present - if (html.startsWith('

') && html.endsWith('

')) { - return fastTextOnly(html.slice(3, -4)) - } - return fastTextOnly(html) - }), - })) - - return { - date: item.date, - schemaChanges: processChanges(item.schemaChanges || []), - previewChanges: processChanges(item.previewChanges || []), - upcomingChanges: processChanges(item.upcomingChanges || []), - } - }) - - const templateData = { - pageTitle: page.title, - pageIntro: intro, - manualContent, - changelogItems, - } - - const templatePath = join(__dirname, '../templates/graphql-changelog.template.md') - const templateContent = readFileSync(templatePath, 'utf8') + const templateContent = loadTemplate(this.templateName) return await renderContent(templateContent, { ...context, @@ -238,87 +114,6 @@ export class GraphQLTransformer implements PageTransformer { }) } - /** - * Transform breaking changes page - */ - private async transformBreakingChanges( - page: Page, - currentVersion: string, - context: Context, - ): Promise { - const { getGraphqlBreakingChanges } = await import('@/graphql/lib/index') - - const schema = getGraphqlBreakingChanges(currentVersion) as BreakingChangesT - - const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' - const manualContent = await this.extractManualContent(page, context) - - const slugger = new GithubSlugger() - - // Process breaking changes by date - const breakingChangesByDate = Object.keys(schema).map((date) => { - const items = schema[date] - const heading = `Changes scheduled for ${date}` - const slug = slugger.slug(heading) - - return { - date, - heading, - slug, - items: items.map((item) => ({ - location: item.location, - description: fastTextOnly(item.description), - reason: fastTextOnly(item.reason), - criticality: item.criticality, - })), - } - }) - - const templateData = { - pageTitle: page.title, - pageIntro: intro, - manualContent, - breakingChangesByDate, - } - - const templatePath = join(__dirname, '../templates/graphql-breaking-changes.template.md') - const templateContent = readFileSync(templatePath, 'utf8') - - return await renderContent(templateContent, { - ...context, - ...templateData, - markdownRequested: true, - }) - } - - /** - * Extract manual content from page markdown - */ - private async extractManualContent(page: Page, context: Context): Promise { - if (!page.markdown) return '' - - const markerIndex = page.markdown.indexOf( - '', - ) - - if (markerIndex <= 0) return '' - - const { content } = matter(page.markdown) - const manualContentMarkerIndex = content.indexOf( - '', - ) - - if (manualContentMarkerIndex <= 0) return '' - - const rawManualContent = content.substring(0, manualContentMarkerIndex).trim() - if (!rawManualContent) return '' - - return await renderContent(rawManualContent, { - ...context, - markdownRequested: true, - }) - } - /** * Prepare a query item for rendering */ diff --git a/src/article-api/transformers/index.ts b/src/article-api/transformers/index.ts index 08722471b6e8..7c56d4b55e9a 100644 --- a/src/article-api/transformers/index.ts +++ b/src/article-api/transformers/index.ts @@ -3,10 +3,15 @@ import { RestTransformer } from './rest-transformer' import { SecretScanningTransformer } from './secret-scanning-transformer' import { CodeQLCliTransformer } from './codeql-cli-transformer' import { AuditLogsTransformer } from './audit-logs-transformer' -import { GraphQLTransformer } from './graphql-transformer' +import { GraphQLIndexTransformer } from './graphql-index-transformer' +import { GraphQLReferenceTransformer } from './graphql-reference-transformer' +import { GraphQLChangelogTransformer } from './graphql-changelog-transformer' +import { GraphQLBreakingChangesTransformer } from './graphql-breaking-changes-transformer' import { GithubAppsTransformer } from './github-apps-transformer' import { WebhooksTransformer } from './webhooks-transformer' import { TocTransformer } from './toc-transformer' +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' @@ -22,10 +27,15 @@ transformerRegistry.register(new RestTransformer()) transformerRegistry.register(new SecretScanningTransformer()) transformerRegistry.register(new CodeQLCliTransformer()) transformerRegistry.register(new AuditLogsTransformer()) -transformerRegistry.register(new GraphQLTransformer()) +transformerRegistry.register(new GraphQLIndexTransformer()) +transformerRegistry.register(new GraphQLReferenceTransformer()) +transformerRegistry.register(new GraphQLChangelogTransformer()) +transformerRegistry.register(new GraphQLBreakingChangesTransformer()) transformerRegistry.register(new GithubAppsTransformer()) transformerRegistry.register(new WebhooksTransformer()) transformerRegistry.register(new TocTransformer()) +transformerRegistry.register(new BespokeLandingTransformer()) +transformerRegistry.register(new JourneyLandingTransformer()) transformerRegistry.register(new CategoryLandingTransformer()) transformerRegistry.register(new DiscoveryLandingTransformer()) transformerRegistry.register(new ProductGuidesTransformer()) diff --git a/src/article-api/transformers/journey-landing-transformer.ts b/src/article-api/transformers/journey-landing-transformer.ts new file mode 100644 index 000000000000..959b28deaf6a --- /dev/null +++ b/src/article-api/transformers/journey-landing-transformer.ts @@ -0,0 +1,115 @@ +import type { Context, Page } from '@/types' +import type { PageTransformer, TemplateData, Section, LinkData, LinkGroup } from './types' +import { renderContent } from '@/content-render/index' +import { loadTemplate } from '@/article-api/lib/load-template' +import { resolvePath } from '@/article-api/lib/resolve-path' +import { getLinkData } from '@/article-api/lib/get-link-data' + +interface JourneyGuide { + href: string + alternativeNextStep?: string +} + +interface JourneyTrack { + id: string + title: string + description: string + guides: JourneyGuide[] +} + +interface JourneyPage extends Page { + journeyTracks?: JourneyTrack[] + children?: string[] +} + +/** + * Transforms journey-landing pages into markdown format. + * Handles journey tracks (grouped learning paths) with guides, + * falling back to children listings when tracks aren't available. + */ +export class JourneyLandingTransformer implements PageTransformer { + templateName = 'landing-page.template.md' + + canTransform(page: Page): boolean { + return page.layout === 'journey-landing' + } + + 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 journeyPage = page as JourneyPage + const languageCode = page.languageCode || 'en' + const sections: Section[] = [] + + // Journey tracks + const journeyTracks = journeyPage.journeyTracks + if (journeyTracks) { + const groups: LinkGroup[] = [] + for (const track of journeyTracks) { + const links = await Promise.all( + (track.guides || []).map(async (guide) => { + const guideHref = guide.href + if (!guideHref) return null + const linkData = await getLinkData( + guideHref, + languageCode, + pathname, + context, + resolvePath, + ) + return linkData + }), + ) + const validLinks = links.filter((l): l is LinkData => l !== null && !!l.href) + if (validLinks.length > 0) { + groups.push({ title: track.title, links: validLinks }) + } + } + + if (groups.length > 0) { + sections.push({ + title: 'Links', + groups, + }) + } + } + + // Children fallback + if (sections.length === 0 && journeyPage.children) { + const links = await Promise.all( + journeyPage.children.map(async (childHref) => { + return await getLinkData(childHref, languageCode, pathname, context, resolvePath) + }), + ) + const validLinks = links.filter((l): l is LinkData => !!l.href) + if (validLinks.length > 0) { + sections.push({ + title: 'Links', + groups: [{ title: null, links: validLinks }], + }) + } + } + + 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/rest-transformer.ts b/src/article-api/transformers/rest-transformer.ts index 5ba981f1a55d..2ef6f845d8c9 100644 --- a/src/article-api/transformers/rest-transformer.ts +++ b/src/article-api/transformers/rest-transformer.ts @@ -2,14 +2,10 @@ import type { Context, Page } from '@/types' import type { PageTransformer } from './types' import type { Operation } from '@/rest/components/types' import { renderContent } from '@/content-render/index' +import { loadTemplate } from '@/article-api/lib/load-template' import matter from '@gr2m/gray-matter' -import { readFileSync } from 'fs' -import { join, dirname } from 'path' -import { fileURLToPath } from 'url' import { fastTextOnly } from '@/content-render/unified/text-only' -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) const DEBUG = process.env.RUNNER_DEBUG === '1' || process.env.DEBUG === '1' /** @@ -17,6 +13,8 @@ const DEBUG = process.env.RUNNER_DEBUG === '1' || process.env.DEBUG === '1' * Converts REST operations and their data into markdown format using a Liquid template */ export class RestTransformer implements PageTransformer { + templateName = 'rest-page.template.md' + canTransform(page: Page): boolean { // Only transform REST pages that are not landing pages // Landing pages (like /en/rest) will be handled by a separate transformer @@ -104,8 +102,7 @@ export class RestTransformer implements PageTransformer { ) // Load and render template - const templatePath = join(__dirname, '../templates/rest-page.template.md') - const templateContent = readFileSync(templatePath, 'utf8') + const templateContent = loadTemplate(this.templateName) // Render the template with Liquid const rendered = await renderContent(templateContent, { diff --git a/src/article-api/transformers/secret-scanning-transformer.ts b/src/article-api/transformers/secret-scanning-transformer.ts index b98f4cdcf717..8bed61b8e2a5 100644 --- a/src/article-api/transformers/secret-scanning-transformer.ts +++ b/src/article-api/transformers/secret-scanning-transformer.ts @@ -4,15 +4,18 @@ import fs from 'fs' import yaml from 'js-yaml' import path from 'path' import { getVersionInfo } from '@/app/lib/constants' -import { liquid } from '@/content-render/index' +import { liquid, renderContent } from '@/content-render/index' import { allVersions } from '@/versions/lib/all-versions' +import { loadTemplate } from '@/article-api/lib/load-template' /** * Transformer for Secret Scanning pages. - * Loads pattern data and converts secret scanning documentation into markdown format. + * Loads pattern data and converts secret scanning documentation into markdown format using a Liquid template. * Used by the Article API to render Secret Scanning documentation dynamically. */ export class SecretScanningTransformer implements PageTransformer { + templateName = 'secret-scanning-page.template.md' + canTransform(page: Page): boolean { return page.autogenerated === 'secret-scanning' } @@ -71,6 +74,22 @@ export class SecretScanningTransformer implements PageTransformer { const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' - return `# ${page.title}\n\n${intro}\n\n${content}` + // Prepare template data + const templateData: Record = { + page: { + title: page.title, + intro, + }, + content, + } + + // Load and render template + const templateContent = loadTemplate(this.templateName) + + return await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) } } diff --git a/src/article-api/transformers/webhooks-transformer.ts b/src/article-api/transformers/webhooks-transformer.ts index 21ff264244fa..d30e84a08fb0 100644 --- a/src/article-api/transformers/webhooks-transformer.ts +++ b/src/article-api/transformers/webhooks-transformer.ts @@ -2,13 +2,16 @@ import type { Context, Page } from '@/types' import type { PageTransformer } from './types' import { renderContent } from '@/content-render/index' import { fastTextOnly } from '@/content-render/unified/text-only' +import { loadTemplate } from '@/article-api/lib/load-template' import matter from '@gr2m/gray-matter' /** * Transformer for Webhooks pages. - * Converts webhook events and payloads into markdown format. + * Converts webhook events and payloads into markdown format using a Liquid template. */ export class WebhooksTransformer implements PageTransformer { + templateName = 'webhooks-page.template.md' + canTransform(page: Page): boolean { return page.autogenerated === 'webhooks' } @@ -26,12 +29,6 @@ export class WebhooksTransformer implements PageTransformer { // Prepare page intro const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' - // Build the page header manually to avoid Context type conflicts - let headerMarkdown = `# ${page.title}\n\n` - if (intro) { - headerMarkdown += `${intro}\n\n` - } - // Prepare manual content let manualContent = '' if (page.markdown) { @@ -51,75 +48,43 @@ export class WebhooksTransformer implements PageTransformer { } } - // Build webhooks sections manually - let webhooksMarkdown = '' - for (const webhook of webhooksData) { - webhooksMarkdown += `## ${webhook.name}\n\n` - - // Summary if available - if (webhook.data.summaryHtml) { - const summaryText = fastTextOnly(webhook.data.summaryHtml) - webhooksMarkdown += `${summaryText}\n\n` - } - - // Availability - if (webhook.data.availability && webhook.data.availability.length > 0) { - webhooksMarkdown += '### Availability\n\n' - for (const avail of webhook.data.availability) { - webhooksMarkdown += `- \`${avail}\`\n` - } - webhooksMarkdown += '\n' - } - - // Webhook payload object section - webhooksMarkdown += '### Webhook payload object\n\n' - - // Available actions - if (webhook.actionTypes.length > 1) { - webhooksMarkdown += '**Action type:** ' - const actions = webhook.actionTypes.map((a) => `\`${a}\``).join(', ') - webhooksMarkdown += `${actions}\n\n` - } - - // Description if available - if (webhook.data.descriptionHtml) { - const descriptionText = fastTextOnly(webhook.data.descriptionHtml) - webhooksMarkdown += `${descriptionText}\n\n` - } - - // Body parameters (payload structure) - if (webhook.data.bodyParameters && webhook.data.bodyParameters.length > 0) { - webhooksMarkdown += '#### Webhook payload object parameters\n\n' - webhooksMarkdown += '| Name | Type | Description |\n' - webhooksMarkdown += '|------|------|-------------|\n' - - for (const param of webhook.data.bodyParameters) { - const name = param.name ? `\`${param.name}\`` : '' - const type = param.type ? `\`${param.type}\`` : '' - // Convert HTML description to plain text - let desc = param.description || '' - if (desc) { - desc = fastTextOnly(desc).replace(/\n/g, ' ').trim() - } - // Add required indicator - if (param.isRequired) { - desc = `**Required.** ${desc}` - } - - webhooksMarkdown += `| ${name} | ${type} | ${desc} |\n` - } - webhooksMarkdown += '\n' - } - - // Payload example if available - if (webhook.data.payloadExample) { - webhooksMarkdown += '### Webhook payload example\n\n' - webhooksMarkdown += '```json\n' - webhooksMarkdown += JSON.stringify(webhook.data.payloadExample, null, 2) - webhooksMarkdown += '\n```\n\n' - } + // Prepare webhooks data for template + const preparedWebhooks = webhooksData.map((webhook) => ({ + name: webhook.name, + actionTypes: webhook.actionTypes, + summary: webhook.data.summaryHtml ? fastTextOnly(webhook.data.summaryHtml) : '', + description: webhook.data.descriptionHtml ? fastTextOnly(webhook.data.descriptionHtml) : '', + availability: webhook.data.availability || [], + bodyParameters: (webhook.data.bodyParameters || []).map((param) => ({ + name: param.name || '', + type: param.type || '', + description: param.description + ? fastTextOnly(param.description).replace(/\n/g, ' ').trim() + : '', + isRequired: param.isRequired || false, + })), + payloadExample: webhook.data.payloadExample + ? JSON.stringify(webhook.data.payloadExample, null, 2) + : null, + })) + + // Prepare template data + const templateData: Record = { + page: { + title: page.title, + intro, + }, + manualContent, + webhooks: preparedWebhooks, } - return `${headerMarkdown + manualContent}\n\n${webhooksMarkdown}` + // Load and render template + const templateContent = loadTemplate(this.templateName) + + return await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) } } diff --git a/src/workflows/fr-add-docs-reviewers-requests.ts b/src/workflows/fr-add-docs-reviewers-requests.ts index 92bd8a871ebc..7b426be1e2d8 100644 --- a/src/workflows/fr-add-docs-reviewers-requests.ts +++ b/src/workflows/fr-add-docs-reviewers-requests.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/no-extraneous-dependencies import { graphql } from '@octokit/graphql' import { diff --git a/src/workflows/projects.ts b/src/workflows/projects.ts index 07048d2cb2ea..398b9e1924b8 100644 --- a/src/workflows/projects.ts +++ b/src/workflows/projects.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/no-extraneous-dependencies import { graphql } from '@octokit/graphql' // Shared functions for managing projects (memex)