diff --git a/content/actions/concepts/security/index.md b/content/actions/concepts/security/index.md index 01603c7e36f4..a53815c86dca 100644 --- a/content/actions/concepts/security/index.md +++ b/content/actions/concepts/security/index.md @@ -12,5 +12,6 @@ children: - /openid-connect - /script-injections - /compromised-runners + - /kubernetes-admissions-controller --- diff --git a/content/actions/concepts/security/kubernetes-admissions-controller.md b/content/actions/concepts/security/kubernetes-admissions-controller.md new file mode 100644 index 000000000000..426ad30c8e50 --- /dev/null +++ b/content/actions/concepts/security/kubernetes-admissions-controller.md @@ -0,0 +1,33 @@ +--- +title: Kubernetes admissions controller +intro: Understand how you can use an admissions controller to enforce artifact attestations in your Kubernetes cluster. +versions: + fpt: '*' + ghec: '*' +--- + +## About Kubernetes admission controller + +[Artifact attestations](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds) enable you to create unfalsifiable provenance and integrity guarantees for the software you build. In turn, people who consume your software can verify where and how your software was built. + +Kubernetes admission controllers are plugins that govern the behavior of the Kubernetes API server. They are commonly used to enforce security policies and best practices in a Kubernetes cluster. + +Using the open source [Sigstore Policy Controller](https://docs.sigstore.dev/policy-controller/overview/) project you can add an admission controller to your Kubernetes cluster that can enforce artifact attestations. This way, you can ensure that only artifacts with valid attestations can be deployed. + +To [install the controller](/actions/how-tos/security-for-github-actions/using-artifact-attestations/enforcing-artifact-attestations-with-a-kubernetes-admission-controller), we offer [two Helm charts](https://github.com/github/artifact-attestations-helm-charts): one for deploying the Sigstore Policy Controller, and another for loading the GitHub trust root and a default policy. + +### About image verification + +When the Policy Controller is installed, it will intercept all image pull requests and verify the attestation for the image. The attestation must be stored in the image registry as an [OCI attached artifact](https://oras.land/docs/concepts/reftypes/) containing a [Sigstore Bundle](https://docs.sigstore.dev/about/bundle/) which contains the attestation and cryptographic material (e.g. certificates and signatures) used to verify the attestation. A verification process is then performed that ensures the image was built with the specified build provenance and matches any policies enabled by the cluster administrator. + +In order for an image to be verifiable, it must have a valid provenance attestation in the registry, which can be done by enabling the `push-to-registry: true` attribute in the `actions/attest-build-provenance` action. See [Generating build provenance for container images](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds#generating-build-provenance-for-container-images) for more details on how to generate attestations for container images. + +### About trust roots and policies + +The Sigstore Policy Controller is primarily configured with trust roots and policies, represented by the Custom Resources `TrustRoot` and `ClusterImagePolicy`. A `TrustRoot` represents a trusted distribution channel for the public key material used to verify attestations. A `ClusterImagePolicy` represents a policy for enforcing attestations on images. + +A `TrustRoot` may also contain a [TUF](https://theupdateframework.io/) repository root, making it possible for your cluster to continuously and securely receive updates to its trusted public key material. If left unspecified, a `ClusterImagePolicy` will by default use the open source Sigstore Public Good Instance's key material. When verifying attestations generated for private repositories, the `ClusterImagePolicy` must reference the GitHub `TrustRoot`. + +## Next steps + +When you're ready to use an admission controller, see [AUTOTITLE](/actions/how-tos/security-for-github-actions/using-artifact-attestations/enforcing-artifact-attestations-with-a-kubernetes-admission-controller). diff --git a/content/actions/get-started/understanding-github-actions.md b/content/actions/get-started/understanding-github-actions.md index 4f6ec8a1adbd..63cdd36d3f2f 100644 --- a/content/actions/get-started/understanding-github-actions.md +++ b/content/actions/get-started/understanding-github-actions.md @@ -78,7 +78,11 @@ For more information, see [AUTOTITLE](/actions/using-jobs). ### Actions -An **action** is a custom application for the {% data variables.product.prodname_actions %} platform that performs a complex but frequently repeated task. Use an action to help reduce the amount of repetitive code that you write in your **workflow** files. An action can pull your Git repository from {% data variables.product.prodname_dotcom %}, set up the correct toolchain for your build environment, or set up the authentication to your cloud provider. +An **action** is a pre-defined, reusable set of jobs or code that performs specific tasks within a **workflow**, reducing the amount of repetitive code you write in your workflow files. Actions can perform tasks such as: + +* Pulling your Git repository from {% data variables.product.prodname_dotcom %} +* Setting up the correct toolchain for your build environment +* Setting up authentication to your cloud provider You can write your own actions, or you can find actions to use in your workflows in the {% data variables.product.prodname_marketplace %}. diff --git a/content/actions/how-tos/administering-github-actions/making-retired-namespaces-available-on-ghecom.md b/content/actions/how-tos/administering-github-actions/making-retired-namespaces-available-on-ghecom.md index 512b77ea9b94..e0f2993e42d5 100644 --- a/content/actions/how-tos/administering-github-actions/making-retired-namespaces-available-on-ghecom.md +++ b/content/actions/how-tos/administering-github-actions/making-retired-namespaces-available-on-ghecom.md @@ -10,7 +10,7 @@ redirect_from: - /actions/administering-github-actions/making-retired-namespaces-available-on-ghecom --- -## About retirement of namespaces +## Overview If you use {% data variables.enterprise.data_residency %}, members of your enterprise can create {% data variables.product.prodname_actions %} workflows that use actions directly from {% data variables.product.prodname_dotcom_the_website %} or [{% data variables.product.prodname_marketplace %}](https://github.com/marketplace?type=actions). diff --git a/content/actions/how-tos/index.md b/content/actions/how-tos/index.md index f514786a7381..3f1930327dbe 100644 --- a/content/actions/how-tos/index.md +++ b/content/actions/how-tos/index.md @@ -15,7 +15,6 @@ children: - /managing-self-hosted-runners - /using-larger-runners - /security-for-github-actions - - /use-cases-and-examples - /administering-github-actions - /monitor-workflows - /troubleshooting-workflows diff --git a/content/actions/how-tos/managing-workflow-runs-and-deployments/managing-workflow-runs/canceling-a-workflow.md b/content/actions/how-tos/managing-workflow-runs-and-deployments/managing-workflow-runs/canceling-a-workflow.md index a741e51c0f66..f75c6516bf75 100644 --- a/content/actions/how-tos/managing-workflow-runs-and-deployments/managing-workflow-runs/canceling-a-workflow.md +++ b/content/actions/how-tos/managing-workflow-runs-and-deployments/managing-workflow-runs/canceling-a-workflow.md @@ -1,20 +1,17 @@ --- title: Canceling a workflow shortTitle: Cancel a workflow -intro: 'You can cancel a workflow run that is in progress. When you cancel a workflow run, {% data variables.product.prodname_dotcom %} cancels all jobs and steps that are a part of that workflow.' +intro: 'You can cancel a workflow run, including all jobs and steps, that is in progress.' versions: fpt: '*' ghes: '*' ghec: '*' +permissions: '{% data reusables.repositories.permissions-statement-write %}' redirect_from: - /actions/managing-workflow-runs/canceling-a-workflow - /actions/managing-workflow-runs-and-deployments/managing-workflow-runs/canceling-a-workflow --- -{% data reusables.actions.enterprise-github-hosted-runners %} - -{% data reusables.repositories.permissions-statement-write %} - ## Canceling a workflow run {% data reusables.repositories.navigate-to-repo %} @@ -24,12 +21,6 @@ redirect_from: 1. In the upper-right corner of the workflow, click **Cancel workflow**. ![Screenshot showing the summary for a workflow that is currently running. The "Cancel workflow" button is highlighted with a dark orange outline.](/assets/images/help/repository/cancel-check-suite-updated.png) -## Steps {% data variables.product.prodname_dotcom %} takes to cancel a workflow run - -When canceling workflow run, you may be running other software that uses resources that are related to the workflow run. To help you free up resources related to the workflow run, it may help to understand the steps {% data variables.product.prodname_dotcom %} performs to cancel a workflow run. +## Next steps -1. To cancel the workflow run, the server re-evaluates `if` conditions for all currently running jobs. If the condition evaluates to `true`, the job will not get canceled. For example, the condition `if: always()` would evaluate to true and the job continues to run. When there is no condition, that is the equivalent of the condition `if: success()`, which only runs if the previous step finished successfully. -1. For jobs that need to be canceled, the server sends a cancellation message to all the runner machines with jobs that need to be canceled. -1. For jobs that continue to run, the server re-evaluates `if` conditions for the unfinished steps. If the condition evaluates to `true`, the step continues to run. You can use the `cancelled` expression to apply a status check of `cancelled()`. For more information see [AUTOTITLE](/actions/learn-github-actions/expressions#cancelled). -1. For steps that need to be canceled, the runner machine sends `SIGINT/Ctrl-C` to the step's entry process (`node` for javascript action, `docker` for container action, and `bash/cmd/pwd` when using `run` in a step). If the process doesn't exit within 7500 ms, the runner will send `SIGTERM/Ctrl-Break` to the process, then wait for 2500 ms for the process to exit. If the process is still running, the runner kills the process tree. -1. After the 5 minutes cancellation timeout period, the server will force terminate all jobs and steps that don't finish running or fail to complete the cancellation process. +To learn about the process {% data variables.product.prodname_dotcom %} uses to cancel a workflow run, as well as the ways you can free up related resources, see [AUTOTITLE](/actions/reference/workflow-cancellation-reference). diff --git a/content/actions/how-tos/security-for-github-actions/using-artifact-attestations/enforcing-artifact-attestations-with-a-kubernetes-admission-controller.md b/content/actions/how-tos/security-for-github-actions/using-artifact-attestations/enforcing-artifact-attestations-with-a-kubernetes-admission-controller.md index 4b7826c9b3fe..ef18bd82447e 100644 --- a/content/actions/how-tos/security-for-github-actions/using-artifact-attestations/enforcing-artifact-attestations-with-a-kubernetes-admission-controller.md +++ b/content/actions/how-tos/security-for-github-actions/using-artifact-attestations/enforcing-artifact-attestations-with-a-kubernetes-admission-controller.md @@ -12,28 +12,6 @@ redirect_from: >[!NOTE] Before proceeding, ensure you have enabled build provenance for container images, including setting the `push-to-registry` attribute in the [`attest-build-provenance` action](https://github.com/actions/attest-build-provenance) as documented in [Generating build provenance for container images](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds#generating-build-provenance-for-container-images). This is required for the Policy Controller to verify the attestation. -## About Kubernetes admission controller - -[Artifact attestations](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds) enable you to create unfalsifiable provenance and integrity guarantees for the software you build. In turn, people who consume your software can verify where and how your software was built. - -Kubernetes admission controllers are plugins that govern the behavior of the Kubernetes API server. They are commonly used to enforce security policies and best practices in a Kubernetes cluster. - -Using the open source [Sigstore Policy Controller](https://docs.sigstore.dev/policy-controller/overview/) project you can add an admission controller to your Kubernetes cluster that can enforce artifact attestations. This way, you can ensure that only artifacts with valid attestations can be deployed. - -To [install the controller](#getting-started-with-kubernetes-admission-controller), we offer [two Helm charts](https://github.com/github/artifact-attestations-helm-charts): one for deploying the Sigstore Policy Controller, and another for loading the GitHub trust root and a default policy. - -### About image verification - -When the Policy Controller is installed, it will intercept all image pull requests and verify the attestation for the image. The attestation must be stored in the image registry as an [OCI attached artifact](https://oras.land/docs/concepts/reftypes/) containing a [Sigstore Bundle](https://docs.sigstore.dev/about/bundle/) which contains the attestation and cryptographic material (e.g. certificates and signatures) used to verify the attestation. A verification process is then performed that ensures the image was built with the specified build provenance and matches any policies enabled by the cluster administrator. - -In order for an image to be verifiable, it must have a valid provenance attestation in the registry, which can be done by enabling the `push-to-registry: true` attribute in the `actions/attest-build-provenance` action. See [Generating build provenance for container images](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds#generating-build-provenance-for-container-images) for more details on how to generate attestations for container images. - -### About trust roots and policies - -The Sigstore Policy Controller is primarily configured with trust roots and policies, represented by the Custom Resources `TrustRoot` and `ClusterImagePolicy`. A `TrustRoot` represents a trusted distribution channel for the public key material used to verify attestations. A `ClusterImagePolicy` represents a policy for enforcing attestations on images. - -A `TrustRoot` may also contain a [TUF](https://theupdateframework.io/) repository root, making it possible for your cluster to continuously and securely receive updates to its trusted public key material. If left unspecified, a `ClusterImagePolicy` will by default use the open source Sigstore Public Good Instance's key material. When verifying attestations generated for private repositories, the `ClusterImagePolicy` must reference the GitHub `TrustRoot`. - ## Getting started with Kubernetes admission controller To set up an admission controller for enforcing GitHub artifact attestations, you need to: diff --git a/content/actions/how-tos/sharing-automations/sharing-actions-and-workflows-from-your-private-repository.md b/content/actions/how-tos/sharing-automations/sharing-actions-and-workflows-from-your-private-repository.md index 9b902e881ef5..51a05294d9be 100644 --- a/content/actions/how-tos/sharing-automations/sharing-actions-and-workflows-from-your-private-repository.md +++ b/content/actions/how-tos/sharing-automations/sharing-actions-and-workflows-from-your-private-repository.md @@ -3,31 +3,28 @@ title: Sharing actions and workflows from your private repository intro: You can share an action or reusable workflow without publishing them publicly. versions: fpt: '*' -type: tutorial topics: - Actions - Action development -shortTitle: Share from your private repository +shortTitle: Share across private repositories redirect_from: - /actions/creating-actions/sharing-actions-and-workflows-from-your-private-repository - /actions/sharing-automations/sharing-actions-and-workflows-from-your-private-repository --- -## About {% data variables.product.prodname_actions %} access to private repositories - -You can share actions and reusable workflows from your private repository, without making them public, by allowing {% data variables.product.prodname_actions %} workflows to access a private repository that contains the action or reusable workflow. - -Any actions or reusable workflows stored in the private repository can be used in workflows defined in other private repositories owned by the same organization or user. Actions and reusable workflows stored in private repositories cannot be used in public repositories. - > [!WARNING] -> * If you make a private repository accessible to {% data variables.product.prodname_actions %} workflows in other repositories, outside collaborators on the other repositories can indirectly access the private repository, even though they do not have direct access to these repositories. The outside collaborators can view logs for workflow runs when actions or workflows from the private repository are used. +> * {% data reusables.actions.outside-collaborators-actions %} > * {% data reusables.actions.scoped-token-note %} ## Sharing actions and workflows from your private repository 1. Store the action or reusable workflow in a private repository. For more information, see [AUTOTITLE](/repositories/creating-and-managing-repositories/about-repositories#about-repository-visibility). -1. Configure the repository to allow access to workflows in other private repositories. For more information, see [AUTOTITLE](/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#allowing-access-to-components-in-a-private-repository). +1. On {% data variables.product.prodname_dotcom %}, navigate to the main page of the private repository. +1. Under your repository name, click **{% octicon "gear" aria-hidden="true" aria-label="gear" %} Settings**. +{% data reusables.repositories.settings-sidebar-actions-general %} +1. To grant access to other private repositories, in the **Access** section at the bottom of the page, select **Accessible from repositories owned by 'USERNAME' user**. +1. Click **Save** to apply the settings. -## Further reading +## Next steps -* [AUTOTITLE](/actions/using-workflows/reusing-workflows) +To reuse your shared workflows, see [AUTOTITLE](/actions/using-workflows/reusing-workflows). diff --git a/content/actions/how-tos/sharing-automations/sharing-actions-and-workflows-with-your-organization.md b/content/actions/how-tos/sharing-automations/sharing-actions-and-workflows-with-your-organization.md index 79cf36d56701..4adf21d9c126 100644 --- a/content/actions/how-tos/sharing-automations/sharing-actions-and-workflows-with-your-organization.md +++ b/content/actions/how-tos/sharing-automations/sharing-actions-and-workflows-with-your-organization.md @@ -3,7 +3,6 @@ title: Sharing actions and workflows with your organization intro: You can share an action or reusable workflow with your organization without publishing the action or workflow publicly. versions: fpt: '*' -type: tutorial topics: - Actions - Action development @@ -13,12 +12,6 @@ redirect_from: - /actions/sharing-automations/sharing-actions-and-workflows-with-your-organization --- -## About {% data variables.product.prodname_actions %} access to private repositories - -You can share actions and reusable workflows within your organization, without publishing them publicly, by allowing {% data variables.product.prodname_actions %} workflows to access a private repository that contains the action or reusable workflow. - -Any actions or reusable workflows stored in the private repository can be used in workflows defined in other private repositories owned by the same organization. Actions and reusable workflows stored in private repositories cannot be used in public repositories. - > [!WARNING] > * {% data reusables.actions.outside-collaborators-actions %} > * {% data reusables.actions.scoped-token-note %} @@ -26,8 +19,12 @@ Any actions or reusable workflows stored in the private repository can be used i ## Sharing actions and workflows with your organization 1. Store the action or reusable workflow in a private repository. For more information, see [AUTOTITLE](/repositories/creating-and-managing-repositories/about-repositories#about-repository-visibility). -1. Configure the repository to allow access to workflows in other private repositories. For more information, see [AUTOTITLE](/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#allowing-access-to-components-in-a-private-repository). +1. On {% data variables.product.prodname_dotcom %}, navigate to the main page of the private repository. +1. Under your repository name, click **{% octicon "gear" aria-hidden="true" aria-label="gear" %} Settings**. +{% data reusables.repositories.settings-sidebar-actions-general %} +1. To grant access to other private repositories in the organization, in the **Access** section at the bottom of the page, select **Accessible from repositories in the 'ORGANIZATION-NAME' organization**. +1. Click **Save** to apply the settings. -## Further reading +## Next steps -* [AUTOTITLE](/actions/using-workflows/reusing-workflows) +To learn how to reuse your shared workflows, see [AUTOTITLE](/actions/using-workflows/reusing-workflows). diff --git a/content/actions/how-tos/use-cases-and-examples/index.md b/content/actions/how-tos/use-cases-and-examples/index.md deleted file mode 100644 index 530a70ace27b..000000000000 --- a/content/actions/how-tos/use-cases-and-examples/index.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Use cases and examples -shortTitle: Use cases and examples -intro: 'Example workflows that demonstrate the features of {% data variables.product.prodname_actions %}.' -versions: - fpt: '*' - ghes: '*' - ghec: '*' -redirect_from: - - /actions/examples - - /actions/use-cases-and-examples -children: - - project-management - - using-containerized-services ---- - diff --git a/content/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/using-pre-written-building-blocks-in-your-workflow.md b/content/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/using-pre-written-building-blocks-in-your-workflow.md index 2f35b9bf51d6..43533b61bd97 100644 --- a/content/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/using-pre-written-building-blocks-in-your-workflow.md +++ b/content/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/using-pre-written-building-blocks-in-your-workflow.md @@ -1,7 +1,7 @@ --- title: Using pre-written building blocks in your workflow shortTitle: Find and customize actions -intro: 'Actions are the building blocks that power your workflow. A workflow can contain actions created by the community, or you can create your own actions directly within your application''s repository. This guide will show you how to discover, use, and customize actions.' +intro: 'You can use and customize pre-written actions to power your workflow.' redirect_from: - /actions/automating-your-workflow-with-github-actions/using-github-marketplace-actions - /actions/automating-your-workflow-with-github-actions/using-actions-from-github-marketplace-in-your-workflow @@ -19,28 +19,6 @@ topics: - Fundamentals --- -{% data reusables.actions.enterprise-github-hosted-runners %} - -## Overview - -You can use pre-written building blocks, called actions, in your workflow. An action is a pre-defined, reusable set of jobs or code that perform specific tasks within a workflow. - -Actions can be: - -* **Reusable:** actions can be used across different workflows and repositories, allowing you to avoid rewriting the same code. -* **Pre-written:** many actions are available in the {% data variables.product.prodname_marketplace %}, covering a wide range of tasks like checking out code, setting up environments, running tests, and deploying applications. -* **Configurable:** you can configure actions with inputs, outputs, and environment variables to tailor them to your specific needs. -* **Community-driven:** you can create your own actions and share them with others or use actions developed by the community. - -The actions you use in your workflow can be defined in: - -* The same repository as your workflow file{% ifversion ghec or ghes %} -* An internal repository within the same enterprise account that is configured to allow access to workflows{% endif %} -* Any public repository -* A published Docker container image on Docker Hub - -{% data variables.product.prodname_marketplace %} is a central location for you to find actions created by the {% data variables.product.prodname_dotcom %} community.{% ifversion fpt or ghec %} [{% data variables.product.prodname_marketplace %} page](https://github.com/marketplace/actions/) enables you to filter for actions by category. {% endif %} - {% data reusables.actions.enterprise-marketplace-actions %} {% data reusables.actions.actions-marketplace-ghecom %} @@ -59,7 +37,12 @@ You can search and browse actions directly in your repository's workflow editor. ## Adding an action to your workflow -You can add an action to your workflow by referencing the action in your workflow file. +You can add an action to your workflow by referencing the action in your workflow file. The actions you use in your workflow can be defined in: + +* The same repository as your workflow file{% ifversion ghec or ghes %} +* An internal repository within the same enterprise account that is configured to allow access to workflows{% endif %} +* Any public repository +* A published Docker container image on Docker Hub You can view the actions referenced in your {% data variables.product.prodname_actions %} workflows as dependencies in the dependency graph of the repository containing your workflows. For more information, see “[About the dependency graph](/code-security/supply-chain-security/understanding-your-software-supply-chain/about-the-dependency-graph).” @@ -182,7 +165,3 @@ outputs: results-file: # id of output description: "Path to results file" ``` - -## Next steps - -To continue learning about {% data variables.product.prodname_actions %}, see [AUTOTITLE](/actions/learn-github-actions/essential-features-of-github-actions). diff --git a/content/actions/index.md b/content/actions/index.md index 74f40ba46a8a..dc0f3a3b207c 100644 --- a/content/actions/index.md +++ b/content/actions/index.md @@ -8,9 +8,8 @@ introLinks: featuredLinks: startHere: - /actions/how-tos/writing-workflows - - /actions/how-tos/use-cases-and-examples + - /actions/tutorials - /actions/concepts/overview/continuous-integration - - /actions/tutorials/deploying-with-github-actions - /packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions guideCards: - /actions/how-tos/writing-workflows/using-workflow-templates @@ -19,7 +18,6 @@ featuredLinks: popular: - /actions/reference/workflow-syntax-for-github-actions - /actions/how-tos/writing-workflows - - /actions/how-tos/use-cases-and-examples changelog: label: actions redirect_from: diff --git a/content/actions/reference/index.md b/content/actions/reference/index.md index 29ca10fcaf6e..ebc2dda4159b 100644 --- a/content/actions/reference/index.md +++ b/content/actions/reference/index.md @@ -27,4 +27,5 @@ children: - /self-hosted-runners-reference - /supplemental-arguments-and-settings - /extending-github-actions-importer-with-custom-transformers + - /workflow-cancellation-reference --- diff --git a/content/actions/reference/workflow-cancellation-reference.md b/content/actions/reference/workflow-cancellation-reference.md new file mode 100644 index 000000000000..ddd3add8d22d --- /dev/null +++ b/content/actions/reference/workflow-cancellation-reference.md @@ -0,0 +1,17 @@ +--- +title: Workflow cancellation reference +shortTitle: Workflow cancellation reference +intro: Find information on the steps {% data variables.product.prodname_dotcom %} takes to cancel a workflow run. +versions: + fpt: '*' + ghes: '*' + ghec: '*' +--- + +When canceling a workflow run, you may be running other software that uses resources related to the workflow run. To help you free up resources related to the workflow run, it may help to understand the steps {% data variables.product.prodname_dotcom %} performs to cancel a workflow run. + +1. To cancel the workflow run, the server re-evaluates `if` conditions for all currently running jobs. If the condition evaluates to `true`, the job will not get canceled. For example, the condition `if: always()` would evaluate to true and the job continues to run. When there is no condition, that is the equivalent of the condition `if: success()`, which only runs if the previous step finished successfully. +1. For jobs that need to be canceled, the server sends a cancellation message to all the runner machines with jobs that need to be canceled. +1. For jobs that continue to run, the server re-evaluates `if` conditions for the unfinished steps. If the condition evaluates to `true`, the step continues to run. You can use the `cancelled` expression to apply a status check of `cancelled()`. For more information, see [AUTOTITLE](/actions/reference/evaluate-expressions-in-workflows-and-actions#cancelled). +1. For steps that need to be canceled, the runner machine sends `SIGINT/Ctrl-C` to the step's entry process (`node` for JavaScript actions, `docker` for container actions, and `bash/cmd/pwd` when using `run` in a step). If the process doesn't exit within 7500 ms, the runner will send `SIGTERM/Ctrl-Break` to the process, then wait for 2500 ms for the process to exit. If the process is still running, the runner kills the process tree. +1. After the 5 minute cancellation timeout period, the server will forcibly terminate all jobs and steps that are still running. diff --git a/content/actions/tutorials/index.md b/content/actions/tutorials/index.md index da8e4a2b00e0..ef15c090931b 100644 --- a/content/actions/tutorials/index.md +++ b/content/actions/tutorials/index.md @@ -9,16 +9,18 @@ versions: children: - /migrating-to-github-actions - /actions-runner-controller + - /project-management + - /using-containerized-services + - /publishing-packages - /creating-an-example-workflow - - /creating-a-docker-container-action - /use-github_token-in-workflows - /creating-a-javascript-action - /creating-a-composite-action - /store-and-share-data - /deploying-with-github-actions - /communicating-with-docker-service-containers - - /publishing-packages redirect_from: - /actions/guides + - /actions/how-tos/use-cases-and-examples --- diff --git a/content/actions/how-tos/use-cases-and-examples/project-management/adding-labels-to-issues.md b/content/actions/tutorials/project-management/adding-labels-to-issues.md similarity index 97% rename from content/actions/how-tos/use-cases-and-examples/project-management/adding-labels-to-issues.md rename to content/actions/tutorials/project-management/adding-labels-to-issues.md index f2bffc143962..aab84fa86fe8 100644 --- a/content/actions/how-tos/use-cases-and-examples/project-management/adding-labels-to-issues.md +++ b/content/actions/tutorials/project-management/adding-labels-to-issues.md @@ -6,6 +6,7 @@ redirect_from: - /actions/guides/adding-labels-to-issues - /actions/managing-issues-and-pull-requests/adding-labels-to-issues - /actions/use-cases-and-examples/project-management/adding-labels-to-issues + - /actions/how-tos/use-cases-and-examples/project-management/adding-labels-to-issues versions: fpt: '*' ghes: '*' diff --git a/content/actions/how-tos/use-cases-and-examples/project-management/closing-inactive-issues.md b/content/actions/tutorials/project-management/closing-inactive-issues.md similarity index 98% rename from content/actions/how-tos/use-cases-and-examples/project-management/closing-inactive-issues.md rename to content/actions/tutorials/project-management/closing-inactive-issues.md index 09a068ebb2c6..ee6d08b23faa 100644 --- a/content/actions/how-tos/use-cases-and-examples/project-management/closing-inactive-issues.md +++ b/content/actions/tutorials/project-management/closing-inactive-issues.md @@ -6,6 +6,7 @@ redirect_from: - /actions/guides/closing-inactive-issues - /actions/managing-issues-and-pull-requests/closing-inactive-issues - /actions/use-cases-and-examples/project-management/closing-inactive-issues + - /actions/how-tos/use-cases-and-examples/project-management/closing-inactive-issues versions: fpt: '*' ghes: '*' diff --git a/content/actions/how-tos/use-cases-and-examples/project-management/commenting-on-an-issue-when-a-label-is-added.md b/content/actions/tutorials/project-management/commenting-on-an-issue-when-a-label-is-added.md similarity index 97% rename from content/actions/how-tos/use-cases-and-examples/project-management/commenting-on-an-issue-when-a-label-is-added.md rename to content/actions/tutorials/project-management/commenting-on-an-issue-when-a-label-is-added.md index 0dac8a7127b4..10f5612a1ba3 100644 --- a/content/actions/how-tos/use-cases-and-examples/project-management/commenting-on-an-issue-when-a-label-is-added.md +++ b/content/actions/tutorials/project-management/commenting-on-an-issue-when-a-label-is-added.md @@ -5,6 +5,7 @@ redirect_from: - /actions/guides/commenting-on-an-issue-when-a-label-is-added - /actions/managing-issues-and-pull-requests/commenting-on-an-issue-when-a-label-is-added - /actions/use-cases-and-examples/project-management/commenting-on-an-issue-when-a-label-is-added + - /actions/how-tos/use-cases-and-examples/project-management/commenting-on-an-issue-when-a-label-is-added versions: fpt: '*' ghes: '*' diff --git a/content/actions/how-tos/use-cases-and-examples/project-management/index.md b/content/actions/tutorials/project-management/index.md similarity index 89% rename from content/actions/how-tos/use-cases-and-examples/project-management/index.md rename to content/actions/tutorials/project-management/index.md index e591fd023836..239b28d951a8 100644 --- a/content/actions/how-tos/use-cases-and-examples/project-management/index.md +++ b/content/actions/tutorials/project-management/index.md @@ -16,5 +16,7 @@ redirect_from: - /actions/use-cases-and-examples/project-management - /actions/how-tos/use-cases-and-examples/project-management/moving-assigned-issues-on-project-boards - /actions/how-tos/use-cases-and-examples/project-management/removing-a-label-when-a-card-is-added-to-a-project-board-column + - /actions/how-tos/use-cases-and-examples/project-management + - /actions/examples --- diff --git a/content/actions/how-tos/use-cases-and-examples/project-management/scheduling-issue-creation.md b/content/actions/tutorials/project-management/scheduling-issue-creation.md similarity index 98% rename from content/actions/how-tos/use-cases-and-examples/project-management/scheduling-issue-creation.md rename to content/actions/tutorials/project-management/scheduling-issue-creation.md index a34436121131..cf2abb5c10d5 100644 --- a/content/actions/how-tos/use-cases-and-examples/project-management/scheduling-issue-creation.md +++ b/content/actions/tutorials/project-management/scheduling-issue-creation.md @@ -6,6 +6,7 @@ redirect_from: - /actions/guides/scheduling-issue-creation - /actions/managing-issues-and-pull-requests/scheduling-issue-creation - /actions/use-cases-and-examples/project-management/scheduling-issue-creation + - /actions/how-tos/use-cases-and-examples/project-management/scheduling-issue-creation versions: fpt: '*' ghes: '*' diff --git a/content/actions/tutorials/creating-a-docker-container-action.md b/content/actions/tutorials/using-containerized-services/creating-a-docker-container-action.md similarity index 99% rename from content/actions/tutorials/creating-a-docker-container-action.md rename to content/actions/tutorials/using-containerized-services/creating-a-docker-container-action.md index 91986b4120a5..21c79dee0999 100644 --- a/content/actions/tutorials/creating-a-docker-container-action.md +++ b/content/actions/tutorials/using-containerized-services/creating-a-docker-container-action.md @@ -9,6 +9,7 @@ redirect_from: - /actions/building-actions/creating-a-docker-container-action - /actions/creating-actions/creating-a-docker-container-action - /actions/sharing-automations/creating-actions/creating-a-docker-container-action + - /actions/tutorials/creating-a-docker-container-action versions: fpt: '*' ghes: '*' diff --git a/content/actions/how-tos/use-cases-and-examples/using-containerized-services/creating-postgresql-service-containers.md b/content/actions/tutorials/using-containerized-services/creating-postgresql-service-containers.md similarity index 99% rename from content/actions/how-tos/use-cases-and-examples/using-containerized-services/creating-postgresql-service-containers.md rename to content/actions/tutorials/using-containerized-services/creating-postgresql-service-containers.md index a7601cac3a1e..0947b80c6867 100644 --- a/content/actions/how-tos/use-cases-and-examples/using-containerized-services/creating-postgresql-service-containers.md +++ b/content/actions/tutorials/using-containerized-services/creating-postgresql-service-containers.md @@ -8,6 +8,7 @@ redirect_from: - /actions/guides/creating-postgresql-service-containers - /actions/using-containerized-services/creating-postgresql-service-containers - /actions/use-cases-and-examples/using-containerized-services/creating-postgresql-service-containers + - /actions/how-tos/use-cases-and-examples/using-containerized-services/creating-postgresql-service-containers versions: fpt: '*' ghes: '*' diff --git a/content/actions/how-tos/use-cases-and-examples/using-containerized-services/creating-redis-service-containers.md b/content/actions/tutorials/using-containerized-services/creating-redis-service-containers.md similarity index 99% rename from content/actions/how-tos/use-cases-and-examples/using-containerized-services/creating-redis-service-containers.md rename to content/actions/tutorials/using-containerized-services/creating-redis-service-containers.md index 6ad9df2474df..b41c277080c9 100644 --- a/content/actions/how-tos/use-cases-and-examples/using-containerized-services/creating-redis-service-containers.md +++ b/content/actions/tutorials/using-containerized-services/creating-redis-service-containers.md @@ -8,6 +8,7 @@ redirect_from: - /actions/guides/creating-redis-service-containers - /actions/using-containerized-services/creating-redis-service-containers - /actions/use-cases-and-examples/using-containerized-services/creating-redis-service-containers + - /actions/how-tos/use-cases-and-examples/using-containerized-services/creating-redis-service-containers versions: fpt: '*' ghes: '*' diff --git a/content/actions/how-tos/use-cases-and-examples/using-containerized-services/index.md b/content/actions/tutorials/using-containerized-services/index.md similarity index 85% rename from content/actions/how-tos/use-cases-and-examples/using-containerized-services/index.md rename to content/actions/tutorials/using-containerized-services/index.md index 0079d24879c3..33949579aa1e 100644 --- a/content/actions/how-tos/use-cases-and-examples/using-containerized-services/index.md +++ b/content/actions/tutorials/using-containerized-services/index.md @@ -12,7 +12,9 @@ redirect_from: - /actions/guides/using-databases-and-service-containers - /actions/using-containerized-services - /actions/use-cases-and-examples/using-containerized-services + - /actions/how-tos/use-cases-and-examples/using-containerized-services children: + - /creating-a-docker-container-action - /creating-postgresql-service-containers - /creating-redis-service-containers --- diff --git a/content/index.md b/content/index.md index 0fe82c9cbd73..0fb5657cf5f3 100644 --- a/content/index.md +++ b/content/index.md @@ -106,11 +106,11 @@ childGroups: octicon: CopilotIcon children: - copilot + - copilot/get-started/plans-for-github-copilot - copilot/how-tos/completions/getting-code-suggestions-in-your-ide-with-github-copilot - - copilot/concepts/prompt-engineering-for-copilot-chat - - copilot/how-tos/chat/asking-github-copilot-questions-in-github - copilot/tutorials/copilot-chat-cookbook - copilot/how-tos/agents/copilot-coding-agent + - copilot/how-tos/custom-instructions - name: CI/CD and DevOps octicon: GearIcon children: diff --git a/package.json b/package.json index f4dd1e63a763..81de7a9faa53 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "unallowed-contributions": "tsx src/workflows/unallowed-contributions.ts", "update-data-and-image-paths": "tsx src/early-access/scripts/update-data-and-image-paths.ts", "update-enterprise-dates": "tsx src/ghes-releases/scripts/update-enterprise-dates.ts", + "update-filepaths": "tsx src/content-render/scripts/update-filepaths.ts", "update-internal-links": "tsx src/links/scripts/update-internal-links.ts", "validate-asset-images": "tsx src/assets/scripts/validate-asset-images.ts", "validate-github-github-docs-urls": "tsx src/links/scripts/validate-github-github-docs-urls/index.ts", diff --git a/src/content-render/scripts/move-content.js b/src/content-render/scripts/move-content.js index ed9a0adb6f4f..aa49550b896e 100755 --- a/src/content-render/scripts/move-content.js +++ b/src/content-render/scripts/move-content.js @@ -172,6 +172,9 @@ async function main(opts, nameTuple) { // the file is a folder or not. It just needs to know the old and new hrefs. changeFeaturedLinks(oldHref, newHref) + // Update any links in ChildGroups on the homepage. + changeHomepageLinks(oldHref, newHref, verbose) + if (!undo) { if (verbose) { console.log( @@ -581,6 +584,23 @@ function changeLearningTracks(filePath, oldHref, newHref) { fs.writeFileSync(filePath, newContent, 'utf-8') } +function changeHomepageLinks(oldHref, newHref, verbose) { + // Can't deserialize and serialize the Yaml because it would lose + // formatting and comments. So regex replace it. + // Homepage childGroup links do not have a leading '/', so we need to remove that. + const homepageOldHref = oldHref.replace('/', '') + const homepageNewHref = newHref.replace('/', '') + const escapedHomepageOldHref = escapeStringRegexp(homepageOldHref) + const regex = new RegExp(`- ${escapedHomepageOldHref}$`, 'gm') + const homepage = path.join(CONTENT_ROOT, 'index.md') + const oldContent = fs.readFileSync(homepage, 'utf-8') + const newContent = oldContent.replace(regex, `- ${homepageNewHref}`) + if (oldContent !== newContent) { + fs.writeFileSync(homepage, newContent, 'utf-8') + if (verbose) console.log(`Updated homepage links`) + } +} + function changeFeaturedLinks(oldHref, newHref) { const allFiles = walk(CONTENT_ROOT, { globs: ['**/*.md'], @@ -588,7 +608,7 @@ function changeFeaturedLinks(oldHref, newHref) { directories: false, }).filter((file) => !file.includes('README.md')) - const regex = new RegExp(`(^|%})${escapeStringRegexp(oldHref)}($|{%)`) + const regex = new RegExp(`(^|%} )${escapeStringRegexp(oldHref)}($| {%)`) for (const file of allFiles) { let changed = false diff --git a/src/content-render/scripts/reconcile-category-dirs-with-ids.js b/src/content-render/scripts/reconcile-category-dirs-with-ids.js deleted file mode 100755 index 244ce53770b2..000000000000 --- a/src/content-render/scripts/reconcile-category-dirs-with-ids.js +++ /dev/null @@ -1,110 +0,0 @@ -// [start-readme] -// -// This script will say which category pages needs to be renamed -// so they match their respective titles (from the front matter) -// -// [end-readme] - -import fs from 'fs' -import path from 'path' -import assert from 'node:assert/strict' - -import walk from 'walk-sync' -import chalk from 'chalk' -import GithubSlugger from 'github-slugger' -import { decode } from 'html-entities' - -import frontmatter from '@/frame/lib/read-frontmatter' -import { renderContent } from '@/content-render/index' -import { allVersions } from '@/versions/lib/all-versions' -import { ROOT } from '@/frame/lib/constants' - -const slugger = new GithubSlugger() - -const contentDir = path.join(ROOT, 'content') - -const INCLUDE_SUBCATEGORIES = Boolean(JSON.parse(process.env.INCLUDE_SUBCATEGORIES || 'false')) - -main() - -async function main() { - const englishCategoryIndices = getEnglishCategoryIndices().filter((name) => { - return INCLUDE_SUBCATEGORIES || name.split(path.sep).length < 5 - }) - - const shouldRename = [] - - for (const categoryIndex of englishCategoryIndices) { - const contents = fs.readFileSync(categoryIndex, 'utf8') - const { data } = frontmatter(contents) - - if (data.allowTitleToDifferFromFilename) { - continue - } - - const categoryDirPath = path.dirname(categoryIndex) - const categoryDirName = path.basename(categoryDirPath) - - const currentVersionObj = allVersions['free-pro-team@latest'] - assert(currentVersionObj, "No current version found for 'free-pro-team@latest'") - const context = { - currentLanguage: 'en', - currentVersionObj, - } - const title = await renderContent(data.title, context, { textOnly: true }) - slugger.reset() - const expectedSlugs = [slugger.slug(decode(title))] - const shortTitle = data.shortTitle - ? await renderContent(data.shortTitle, context, { textOnly: true }) - : '' - if (shortTitle && shortTitle !== title) { - expectedSlugs.push(slugger.slug(decode(shortTitle))) - } - - // If the directory name already matches the expected slug, bail out now - if (expectedSlugs.includes(categoryDirName)) continue - - // Figure out the new path for the category - const categoryDirParentDir = path.dirname(categoryDirPath) - const newPath = path.join(categoryDirParentDir, expectedSlugs.at(-1)) - - const oldRelativePath = path.relative(ROOT, categoryDirPath) - const newRelativePath = path.relative(ROOT, newPath) - shouldRename.push({ oldRelativePath, newRelativePath }) - } - - if (shouldRename.length > 0) { - console.log( - chalk.yellow( - `${shouldRename.length} ${ - shouldRename.length === 1 ? 'category' : 'categories' - } need to be renamed because their title doesn't match their directory name.`, - ), - ) - console.log(chalk.dim('Run the following commands to rename them:')) - - for (const { oldRelativePath, newRelativePath } of shouldRename) { - console.log( - `./src/content-render/scripts/move-content.js ${oldRelativePath} ${newRelativePath}`, - ) - } - } else { - console.log(chalk.green('No categories need to be renamed! 🎉')) - } -} - -function getEnglishCategoryIndices() { - const walkOptions = { - globs: ['*/*/**/index.md'], - ignore: [ - '{rest,graphql,developers}/**', - 'enterprise/admin/index.md', - '**/articles/**', - '**/early-access/**', - ], - directories: false, - includeBasePath: true, - } - - return walk(contentDir, walkOptions) -} diff --git a/src/content-render/scripts/reconcile-filenames-with-ids.js b/src/content-render/scripts/reconcile-filenames-with-ids.js deleted file mode 100755 index 94796101fc67..000000000000 --- a/src/content-render/scripts/reconcile-filenames-with-ids.js +++ /dev/null @@ -1,80 +0,0 @@ -// [start-readme] -// -// An automated test checks for discrepancies between filenames and [autogenerated heading IDs](https://www.npmjs.com/package/remark-autolink-headings). -// If the test fails, a human needs to run this script to update the filenames. -// -// **This script is not currently supported on Windows.** -// -// [end-readme] - -import fs from 'fs' -import path from 'path' -import walk from 'walk-sync' -import GithubSlugger from 'github-slugger' -import { decode } from 'html-entities' -import frontmatter from '@/frame/lib/read-frontmatter' -import { execFileSync } from 'child_process' -import addRedirectToFrontmatter from '@/redirects/scripts/helpers/add-redirect-to-frontmatter' - -const slugger = new GithubSlugger() - -const contentDir = path.join(process.cwd(), 'content') - -const contentFiles = walk(contentDir, { includeBasePath: true, directories: false }).filter( - (file) => { - return file.endsWith('.md') && !file.endsWith('index.md') && !file.includes('README') - }, -) - -// TODO fix path separators in the redirect -if (process.platform.startsWith('win')) { - console.log('This script cannot be run on Windows at this time! Exiting...') - process.exit() -} - -contentFiles.forEach((oldFullPath) => { - const { data, content } = frontmatter(fs.readFileSync(oldFullPath, 'utf8')) - - // skip pages with frontmatter flag - if (data.allowTitleToDifferFromFilename) return - - // Slugify the title of each article, where: - // * title = Foo bar - // * slug = foo-bar - // Also allow for the slugified shortTitle to match the filename. - slugger.reset() - const slugTitle = slugger.slug(decode(data.title)) - const slugShortTitle = slugger.slug(decode(data.shortTitle)) - const allowedSlugs = [slugTitle, slugShortTitle] - - // get the basename of each file - // where file = content/foo-bar.md - // and basename = foo-bar - const basename = path.basename(oldFullPath, '.md') - - // If the basename is one of the allowed slugs, we're all set here. - if (allowedSlugs.includes(basename)) return - - // otherwise rename the file using the slug - const newFullPath = oldFullPath.replace(basename, slugShortTitle || slugTitle) - - const oldContentPath = path.relative(process.cwd(), oldFullPath) - const newContentPath = path.relative(process.cwd(), newFullPath) - - const gitStatusOfFile = execFileSync('git', ['status', '--porcelain', oldContentPath]).toString() - - // if file is untracked, do a regular mv; otherwise do a git mv - if (gitStatusOfFile.includes('??')) { - execFileSync('mv', [oldContentPath, newContentPath]) - } else { - execFileSync('git', ['mv', oldContentPath, newContentPath]) - } - - // then add the old path to the redirect_from frontmatter - // TODO fix path separators on Windows (e.g. \github\extending-github\about-webhooks) - const redirect = path.join('/', path.relative(contentDir, oldFullPath).replace(/.md$/, '')) - data.redirect_from = addRedirectToFrontmatter(data.redirect_from, redirect) - - // update the file - fs.writeFileSync(newFullPath, frontmatter.stringify(content, data)) -}) diff --git a/src/content-render/scripts/update-filepaths.ts b/src/content-render/scripts/update-filepaths.ts new file mode 100755 index 000000000000..c2acef0ba330 --- /dev/null +++ b/src/content-render/scripts/update-filepaths.ts @@ -0,0 +1,269 @@ +// [start-readme] +// +// Run this script to update filepaths to match short titles (or titles as a fallback). +// Use +// npm run-script -- update-filepaths --help +// +// [end-readme] + +import fs from 'fs' +import path from 'path' +import { program } from 'commander' +import GithubSlugger from 'github-slugger' +import { decode } from 'html-entities' +import { execFileSync } from 'child_process' +import walkFiles from '@/workflows/walk-files' +import frontmatter from '@/frame/lib/read-frontmatter' +import { renderContent } from '@/content-render/index' +import fpt from '@/versions/lib/non-enterprise-default-version' +import { allVersions } from '@/versions/lib/all-versions' +import type { PageFrontmatter, Context } from '@/types' + +interface ScriptOptions { + force?: boolean + excludeDirs?: boolean + paths?: string[] + dryRun?: boolean + verbose: boolean +} + +const context: Context = { + currentLanguage: 'en', + currentVersionObj: allVersions[fpt], +} + +program + .description( + 'Update filepaths to match short titles, unless frontmatter override is present. Processes both files and directories by default.', + ) + .option('-f, --force', 'Update paths even if frontmatter override is present') + .option('-e, --exclude-dirs', 'Exclude directories') + .option( + '-p, --paths [paths...]', + `One or more specific paths to process (e.g., copilot or content/copilot/how-tos/file.md)`, + ) + .option('-d, --dry-run', 'Preview changes without actually making them') + .option('-v, --verbose', 'Verbose') + .parse(process.argv) + +const options: ScriptOptions = program.opts() + +const isDirectoryCheck = (file: string): boolean => file.endsWith('index.md') + +// The script takes about 2 seconds per file, so divide by 30 instead of 60 to get the minutes. +const estimateScriptMinutes = (numberOfFiles: number): string => { + const estNum = Math.round(numberOfFiles / 30) + return estNum === 0 ? '<1' : estNum.toString() +} + +async function main(): Promise { + const slugger = new GithubSlugger() + const contentDir: string = path.join(process.cwd(), 'content') + // Filter to get all the content files we want to read in. + // Then sort them from longest > shortest so we can do the file moves in order. + const filesToProcess: string[] = sortFiles(filterFiles(contentDir, options)) + + if (filesToProcess.length === 0) { + console.log('No files to process') + return + } + + if (!options.dryRun) { + const estimate = estimateScriptMinutes(filesToProcess.length) + console.log(`Processing ${filesToProcess.length} files`) + console.log(`Estimated time: ${estimate} min\n`) + } + + // Process files sequentially to maintain the correct order of operations. + // Files must be moved before directories, and directories must be moved + // from deepest to shallowest to avoid path conflicts during the move operations. + // The result is rather slow, but an asynchronous approach that ensures + // sequential processing would not be faster. + for (const file of filesToProcess) { + try { + slugger.reset() + + const result = await processFile(file, slugger, options) + if (!result) continue + + moveFile(result, options) + } catch (error) { + console.error(`Failed to process ${file}:`, error) + } + } +} + +async function processFile( + file: string, + slugger: GithubSlugger, + options: ScriptOptions, +): Promise { + const { data } = frontmatter(fs.readFileSync(file, 'utf8')) as unknown as { + data: PageFrontmatter + } + + const isDirectory = isDirectoryCheck(file) + + // Assess the frontmatter and other conditions to determine if we want to process the path. + const processPage: boolean = determineProcessStatus(data, isDirectory, options) + if (!processPage) return null + + let stringToSlugify: string = data.shortTitle || data.title + + // Check if we need to process Liquid + if (stringToSlugify.includes('{%')) { + stringToSlugify = await renderContent(stringToSlugify, context, { textOnly: true }) + } + + // Slugify the short title of each article. + // Where: shortTitle = Foo bar + // Returns: slug = foo-bar + // Fall back to title if shortTitle doesn't exist. + const slug: string = slugger.slug(decode(stringToSlugify)) + + // Get the basename, depending on whether it's a file or dir. + let basename: string + if (isDirectory) { + // Where: content location = content/foobar/index.md + // Returns: basename = foobar + basename = path.basename(path.dirname(file)) + } else { + // Where: content location = content/foobar.md + // Returns: basename = foobar + basename = path.basename(file, '.md') + } + + // If slug and basename already match, all set here. Return early. + if (slug === basename) return null + + // Build the new path based on file type. + const newPath = isDirectory + ? path.join(path.dirname(path.dirname(file)), slug, 'index.md') + : path.join(path.dirname(file), `${slug}.md`) + + // Get relative paths and adjust for directories. + const getContentPath = (filePath: string): string => { + const relativePath = path.relative(process.cwd(), filePath) + return isDirectory ? path.dirname(relativePath) : relativePath + } + + const contentPath = getContentPath(file) + const newContentPath = getContentPath(newPath) + + return [contentPath, newContentPath] +} + +function moveFile(result: string[], options: ScriptOptions): void { + const [contentPath, newContentPath] = result + + if (options.dryRun) { + console.log('Move:\n', contentPath, '\nto:\n', newContentPath, '\n') + return + } + + // Call out to well-tested move-content script for the moving and redirect adding functions. + const stdout = execFileSync( + 'tsx', + [ + 'src/content-render/scripts/move-content.js', + '--no-git', + '--verbose', + contentPath, + newContentPath, + ], + { encoding: 'utf8' }, + ) + + // Grab just the "Moving..." and "Renamed..." output from stdout; otherwise output is too noisy. + const moveMsg = stdout.split('\n').find((l) => l.startsWith('Moving') || l.startsWith('Renamed')) + if (moveMsg && !options.verbose) { + console.log(moveMsg, '\n') + } else { + console.log(stdout, '\n') + } +} + +function sortFiles(filesArray: string[]): string[] { + // The order of operations is important. + // We need to return an array so that the moving operations happens in this order: + // 1. Filepaths + // 2. Deepest subdirectory path + // 3. Shallowest subdirectory path (up to category level, e.g., content/product/category) + return filesArray.toSorted((a, b) => { + // If A is a file and B is a directory, A comes first (negative) + if (!isDirectoryCheck(a) && isDirectoryCheck(b)) { + return -1 + } + // If A is a directory and B is a file, B comes first (positive) + if (isDirectoryCheck(a) && !isDirectoryCheck(b)) { + return 1 + } + // If A and B are both files, neutral + if (!isDirectoryCheck(a) && !isDirectoryCheck(b)) { + return 0 + } + // If both are directories, sort by depth (deepest first) + if (isDirectoryCheck(a) && isDirectoryCheck(b)) { + const aDepth = a.split(path.sep).length + const bDepth = b.split(path.sep).length + return bDepth - aDepth // Deeper paths first + } + + // This should never be reached, but return 0 for safety + return 0 + }) +} + +function filterFiles(contentDir: string, options: ScriptOptions) { + return walkFiles(contentDir, ['.md']).filter((file: string) => { + // Never move readmes + if (file.endsWith('README.md')) return false + // Never move early access files + if (file.includes('early-access')) return false + // Never move the homepage (content/index.md) + if (path.relative(contentDir, file) === 'index.md') return false + // Never move product landings (content/foo/index.md) + if (path.relative(contentDir, file).split(path.sep)[1] === 'index.md') return false + + // If no specific paths are passed, we are done filtering. + if (!options.paths) return true + + return options.paths.some((p: string) => { + // Allow either a full content path like "content/foo/bar.md" + // or a top-level directory name like "copilot" + if (!p.startsWith('content')) { + p = path.join('content', p) + } + if (!fs.existsSync(p)) { + console.error(`${p} not found`) + process.exit(1) + } + if (path.relative(process.cwd(), file).startsWith(p)) return true + return false + }) + }) +} + +function determineProcessStatus( + data: PageFrontmatter, + isDirectory: boolean, + options: ScriptOptions, +): boolean { + // Assess the conditions in this order: + // If it's a directory AND we're excluding dirs, do not process it no matter what. + if (isDirectory && options.excludeDirs) { + return false + } + // If the force option is passed, process it no matter what. + if (options.force) { + return true + } + // If the page has the override set, do not process it. + if (data.allowTitleToDifferFromFilename) { + return false + } + // In all other cases, process it. + return true +} + +main() diff --git a/src/frame/tests/pages.js b/src/frame/tests/pages.js index b5a1b1d0a2ef..1f3eee1cf54c 100644 --- a/src/frame/tests/pages.js +++ b/src/frame/tests/pages.js @@ -119,7 +119,7 @@ describe('pages module', () => { nonMatches.length === 1 ? 'file' : 'files' } that do not match their slugified titles.\n ${nonMatches.join('\n')}\n - To fix, run src/content-render/scripts/reconcile-filenames-with-ids.js\n\n` + To fix, run: npm run-script update-filepaths --paths [FILEPATHS]\n\n` expect(nonMatches.length, message).toBe(0) }) diff --git a/src/rest/components/RestCodeSamples.tsx b/src/rest/components/RestCodeSamples.tsx index d6c0747eca05..fe8346673de8 100644 --- a/src/rest/components/RestCodeSamples.tsx +++ b/src/rest/components/RestCodeSamples.tsx @@ -71,6 +71,7 @@ export function RestCodeSamples({ operation, slug, heading }: Props) { javascript: getJSExample(operation, sample, currentVersion, allVersions), ghcli: getGHExample(operation, sample, currentVersion, allVersions), response: sample.response, + request: sample.request, })) // Menu options for the language selector @@ -98,18 +99,46 @@ export function RestCodeSamples({ operation, slug, heading }: Props) { // there's more than one example and if the media types aren't all the same // for the examples (e.g. if all examples have content type `application/json`, // we won't show that information in the menu items). - const showExampleOptionMediaType = + const responseContentTypesDiffer = languageExamples.length > 1 && !languageExamples.every( (example) => example.response.contentType === languageExamples[0].response.contentType, ) - const exampleSelectOptions = languageExamples.map((example, index) => ({ - text: showExampleOptionMediaType - ? `${example.description} (${example.response.contentType})` - : example.description, - // maps to the index of the example in the languageExamples array - languageIndex: index, - })) + + // Check if request content types differ between examples + const requestContentTypesDiffer = + languageExamples.length > 1 && + !languageExamples.every( + (example) => example.request?.contentType === languageExamples[0].request?.contentType, + ) + + const showExampleOptionMediaType = responseContentTypesDiffer || requestContentTypesDiffer + + const exampleSelectOptions = languageExamples.map((example, index) => { + const requestContentType = example.request?.contentType + const responseContentType = example.response.contentType + + let text = example.description + + if (showExampleOptionMediaType) { + if (requestContentTypesDiffer && responseContentTypesDiffer) { + // Show both request and response content types + text = `${example.description} (${requestContentType} → ${responseContentType})` + } else if (requestContentTypesDiffer) { + // Show only request content type + text = `${example.description} (${requestContentType})` + } else if (responseContentTypesDiffer) { + // Show only response content type + text = `${example.description} (${responseContentType})` + } + } + + return { + text, + // maps to the index of the example in the languageExamples array + languageIndex: index, + } + }) const [selectedLanguage, setSelectedLanguage] = useState(languageSelectOptions[0]) const [selectedExample, setSelectedExample] = useState(exampleSelectOptions[0]) diff --git a/src/rest/tests/content-type-logic.js b/src/rest/tests/content-type-logic.js new file mode 100644 index 000000000000..4f726469d872 --- /dev/null +++ b/src/rest/tests/content-type-logic.js @@ -0,0 +1,186 @@ +import { describe, expect, test } from 'vitest' + +describe('Request Content Type Logic', () => { + // Helper function to extract the logic from RestCodeSamples + function shouldShowRequestContentType(codeExamples) { + const requestContentTypesDiffer = + codeExamples.length > 1 && + !codeExamples.every( + (example) => example.request?.contentType === codeExamples[0].request?.contentType, + ) + return requestContentTypesDiffer + } + + function shouldShowResponseContentType(codeExamples) { + const responseContentTypesDiffer = + codeExamples.length > 1 && + !codeExamples.every( + (example) => example.response?.contentType === codeExamples[0].response?.contentType, + ) + return responseContentTypesDiffer + } + + function generateExampleOptions(codeExamples) { + const requestContentTypesDiffer = shouldShowRequestContentType(codeExamples) + const responseContentTypesDiffer = shouldShowResponseContentType(codeExamples) + const showExampleOptionMediaType = responseContentTypesDiffer || requestContentTypesDiffer + + return codeExamples.map((example, index) => { + const requestContentType = example.request?.contentType + const responseContentType = example.response?.contentType + + let text = example.request?.description || `Example ${index + 1}` + + if (showExampleOptionMediaType) { + if (requestContentTypesDiffer && responseContentTypesDiffer) { + // Show both request and response content types + text = `${text} (${requestContentType} → ${responseContentType})` + } else if (requestContentTypesDiffer) { + // Show only request content type + text = `${text} (${requestContentType})` + } else if (responseContentTypesDiffer) { + // Show only response content type + text = `${text} (${responseContentType})` + } + } + + return text + }) + } + + test('detects request content types differ correctly', () => { + const codeExamples = [ + { + request: { contentType: 'text/plain', description: 'Example' }, + response: { contentType: 'text/html' }, + }, + { + request: { contentType: 'text/x-markdown', description: 'Rendering markdown' }, + response: { contentType: 'text/html' }, + }, + ] + + expect(shouldShowRequestContentType(codeExamples)).toBe(true) + expect(shouldShowResponseContentType(codeExamples)).toBe(false) + }) + + test('detects response content types differ correctly', () => { + const codeExamples = [ + { + request: { contentType: 'application/json', description: 'JSON example' }, + response: { contentType: 'application/json' }, + }, + { + request: { contentType: 'application/json', description: 'Another JSON example' }, + response: { contentType: 'text/html' }, + }, + ] + + expect(shouldShowRequestContentType(codeExamples)).toBe(false) + expect(shouldShowResponseContentType(codeExamples)).toBe(true) + }) + + test('generates correct options for markdown/raw scenario', () => { + const markdownRawExamples = [ + { + request: { + contentType: 'text/plain', + description: 'Example', + }, + response: { + contentType: 'text/html', + }, + }, + { + request: { + contentType: 'text/x-markdown', + description: 'Rendering markdown', + }, + response: { + contentType: 'text/html', + }, + }, + ] + + const options = generateExampleOptions(markdownRawExamples) + + expect(options).toEqual(['Example (text/plain)', 'Rendering markdown (text/x-markdown)']) + }) + + test('generates correct options when both request and response differ', () => { + const mixedExamples = [ + { + request: { + contentType: 'application/json', + description: 'JSON request', + }, + response: { + contentType: 'application/json', + }, + }, + { + request: { + contentType: 'text/plain', + description: 'Plain text request', + }, + response: { + contentType: 'text/html', + }, + }, + ] + + const options = generateExampleOptions(mixedExamples) + + expect(options).toEqual([ + 'JSON request (application/json → application/json)', + 'Plain text request (text/plain → text/html)', + ]) + }) + + test('does not show content types when they are all the same', () => { + const sameContentTypeExamples = [ + { + request: { + contentType: 'application/json', + description: 'First example', + }, + response: { + contentType: 'application/json', + }, + }, + { + request: { + contentType: 'application/json', + description: 'Second example', + }, + response: { + contentType: 'application/json', + }, + }, + ] + + const options = generateExampleOptions(sameContentTypeExamples) + + expect(options).toEqual(['First example', 'Second example']) + }) + + test('handles single example correctly', () => { + const singleExample = [ + { + request: { + contentType: 'application/json', + description: 'Only example', + }, + response: { + contentType: 'application/json', + }, + }, + ] + + expect(shouldShowRequestContentType(singleExample)).toBe(false) + expect(shouldShowResponseContentType(singleExample)).toBe(false) + + const options = generateExampleOptions(singleExample) + expect(options).toEqual(['Only example']) + }) +}) diff --git a/src/rest/tests/rendering.js b/src/rest/tests/rendering.js index c9f6021d064a..6c078d6d608f 100644 --- a/src/rest/tests/rendering.js +++ b/src/rest/tests/rendering.js @@ -120,6 +120,29 @@ describe('REST references docs', () => { } } }) + + test('markdown/raw endpoint shows request content types in example selector', async () => { + // Test the specific endpoint that has multiple examples with different request content types + const $ = await getDOM('/en/rest/markdown/markdown?apiVersion=2022-11-28') + + // Find the render raw mode operation section by its specific ID + const rawModeSection = $('#render-a-markdown-document-in-raw-mode--code-samples').parent() + expect(rawModeSection.length).toBeGreaterThan(0) + + // Should have an example selector dropdown since there are multiple examples + const exampleSelector = rawModeSection.find('select[aria-labelledby], select').first() + expect(exampleSelector.length).toBe(1) + + // Get the option texts from the dropdown + const optionTexts = exampleSelector + .find('option') + .map((i, option) => $(option).text().trim()) + .get() + .filter((text) => text.length > 0) + + // Should show request content types since they differ between examples + expect(optionTexts).toEqual(['Example (text/plain)', 'Rendering markdown (text/x-markdown)']) + }) }) function formatErrors(differences) {