1
0
mirror of synced 2026-01-10 18:02:53 -05:00

Merge pull request #33390 from github/repo-sync

Repo sync
This commit is contained in:
docs-bot
2024-06-07 05:36:18 -07:00
committed by GitHub
23 changed files with 521 additions and 231 deletions

View File

@@ -52,7 +52,7 @@ jobs:
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
with:
path: external-link-checker-db.json
key: external-link-checker-${{ hashFiles('src/links/scripts/rendered-content-link-checker.js') }}
key: external-link-checker-${{ hashFiles('src/links/scripts/rendered-content-link-checker.ts') }}
- name: Insight into external link checker DB json file (before)
run: |
@@ -87,7 +87,7 @@ jobs:
EXTERNAL_SERVER_ERRORS_AS_WARNINGS: true
FAIL_ON_FLAW: false
timeout-minutes: 30
run: node src/links/scripts/rendered-content-link-checker.js
run: npm run rendered-content-link-checker
- name: Insight into external link checker DB json file (after)
run: |

View File

@@ -50,4 +50,4 @@ jobs:
# been loaded.
ENABLED_LANGUAGES: en
FAIL_ON_FLAW: true
run: node src/links/scripts/rendered-content-link-checker.js
run: npm run rendered-content-link-checker

View File

@@ -43,7 +43,7 @@ You can access contexts using the expression syntax. For more information, see "
| `strategy` | `object` | Information about the matrix execution strategy for the current job. For more information, see [`strategy` context](#strategy-context). |
| `matrix` | `object` | Contains the matrix properties defined in the workflow that apply to the current job. For more information, see [`matrix` context](#matrix-context). |
| `needs` | `object` | Contains the outputs of all jobs that are defined as a dependency of the current job. For more information, see [`needs` context](#needs-context). |
| `inputs` | `object` | Contains the inputs of a reusable {% ifversion actions-unified-inputs %}or manually triggered {% endif %}workflow. For more information, see [`inputs` context](#inputs-context). |
| `inputs` | `object` | Contains the inputs of a reusable or manually triggered workflow. For more information, see [`inputs` context](#inputs-context). |
As part of an expression, you can access context information using one of two syntaxes.
@@ -90,40 +90,40 @@ The following table indicates where each context and special function can be use
| Workflow key | Context | Special functions |
| ---- | ------- | ----------------- |
| <code>run-name</code> | <code>github, inputs, vars</code> | None |
| <code>concurrency</code> | <code>github, inputs, vars</code> | None |
| <code>env</code> | <code>github, secrets, inputs, vars</code> | None |
| <code>jobs.&lt;job_id&gt;.concurrency</code> | <code>github, needs, strategy, matrix, inputs, vars</code> | None |
| <code>jobs.&lt;job_id&gt;.container</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.container.credentials</code> | <code>github, needs, strategy, matrix, env, vars, secrets, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.container.env.&lt;env_id&gt;</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.container.image</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.continue-on-error</code> | <code>github, needs, strategy, vars, matrix, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.defaults.run</code> | <code>github, needs, strategy, matrix, env, vars, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.env</code> | <code>github, needs, strategy, matrix, vars, secrets, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.environment</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.environment.url</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, steps, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.if</code> | <code>github, needs, vars, inputs</code> | <code>always, cancelled, success, failure</code> |
| <code>jobs.&lt;job_id&gt;.name</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.outputs.&lt;output_id&gt;</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.runs-on</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.secrets.&lt;secrets_id&gt;</code> | <code>github, needs,{% ifversion actions-reusable-workflow-matrix %} strategy, matrix,{% endif %} secrets{% ifversion actions-unified-inputs %}, inputs{% endif %}, vars</code> | None |
| <code>jobs.&lt;job_id&gt;.services</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.services.&lt;service_id&gt;.credentials</code> | <code>github, needs, strategy, matrix, env, vars, secrets, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.services.&lt;service_id&gt;.env.&lt;env_id&gt;</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.steps.continue-on-error</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
| <code>jobs.&lt;job_id&gt;.steps.env</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
| <code>jobs.&lt;job_id&gt;.steps.if</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, steps, inputs</code> | <code>always, cancelled, success, failure, hashFiles</code> |
| <code>jobs.&lt;job_id&gt;.steps.name</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
| <code>jobs.&lt;job_id&gt;.steps.run</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
| <code>jobs.&lt;job_id&gt;.steps.timeout-minutes</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
| <code>jobs.&lt;job_id&gt;.steps.with</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
| <code>jobs.&lt;job_id&gt;.steps.working-directory</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
| <code>jobs.&lt;job_id&gt;.strategy</code> | <code>github, needs, vars, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.timeout-minutes</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
| <code>jobs.&lt;job_id&gt;.with.&lt;with_id&gt;</code> | <code>github, needs{% ifversion actions-reusable-workflow-matrix %}, strategy, matrix{% endif %}{% ifversion actions-unified-inputs %}, inputs{% endif %}, vars</code> | None |
| <code>on.workflow_call.inputs.&lt;inputs_id&gt;.default</code> | <code>github{% ifversion actions-unified-inputs %}, inputs{% endif %}, vars</code> | None |
| <code>on.workflow_call.outputs.&lt;output_id&gt;.value</code> | <code>github, jobs, vars, inputs</code> | None |
| `run-name` | `github, inputs, vars` | None |
| `concurrency` | `github, inputs, vars` | None |
| `env` | `github, secrets, inputs, vars` | None |
| `jobs.<job_id>.concurrency` | `github, needs, strategy, matrix, inputs, vars` | None |
| `jobs.<job_id>.container` | `github, needs, strategy, matrix, vars, inputs` | None |
| `jobs.<job_id>.container.credentials` | `github, needs, strategy, matrix, env, vars, secrets, inputs` | None |
| `jobs.<job_id>.container.env.<env_id>` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, inputs` | None |
| `jobs.<job_id>.container.image` | `github, needs, strategy, matrix, vars, inputs` | None |
| `jobs.<job_id>.continue-on-error` | `github, needs, strategy, vars, matrix, inputs` | None |
| `jobs.<job_id>.defaults.run` | `github, needs, strategy, matrix, env, vars, inputs` | None |
| `jobs.<job_id>.env` | `github, needs, strategy, matrix, vars, secrets, inputs` | None |
| `jobs.<job_id>.environment` | `github, needs, strategy, matrix, vars, inputs` | None |
| `jobs.<job_id>.environment.url` | `github, needs, strategy, matrix, job, runner, env, vars, steps, inputs` | None |
| `jobs.<job_id>.if` | `github, needs, vars, inputs` | `always, cancelled, success, failure` |
| `jobs.<job_id>.name` | `github, needs, strategy, matrix, vars, inputs` | None |
| `jobs.<job_id>.outputs.<output_id>` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs` | None |
| `jobs.<job_id>.runs-on` | `github, needs, strategy, matrix, vars, inputs` | None |
| `jobs.<job_id>.secrets.<secrets_id>` | `github, needs, strategy, matrix, secrets, inputs, vars` | None |
| `jobs.<job_id>.services` | `github, needs, strategy, matrix, vars, inputs` | None |
| `jobs.<job_id>.services.<service_id>.credentials` | `github, needs, strategy, matrix, env, vars, secrets, inputs` | None |
| `jobs.<job_id>.services.<service_id>.env.<env_id>` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, inputs` | None |
| `jobs.<job_id>.steps.continue-on-error` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs` | `hashFiles` |
| `jobs.<job_id>.steps.env` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs` | `hashFiles` |
| `jobs.<job_id>.steps.if` | `github, needs, strategy, matrix, job, runner, env, vars, steps, inputs` | `always, cancelled, success, failure, hashFiles` |
| `jobs.<job_id>.steps.name` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs` | `hashFiles` |
| `jobs.<job_id>.steps.run` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs` | `hashFiles` |
| `jobs.<job_id>.steps.timeout-minutes` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs` | `hashFiles` |
| `jobs.<job_id>.steps.with` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs` | `hashFiles` |
| `jobs.<job_id>.steps.working-directory` | `github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs` | `hashFiles` |
| `jobs.<job_id>.strategy` | `github, needs, vars, inputs` | None |
| `jobs.<job_id>.timeout-minutes` | `github, needs, strategy, matrix, vars, inputs` | None |
| `jobs.<job_id>.with.<with_id>` | `github, needs, strategy, matrix, inputs, vars` | None |
| `on.workflow_call.inputs.<inputs_id>.default` | `github, inputs, vars` | None |
| `on.workflow_call.outputs.<output_id>.value` | `github, jobs, vars, inputs` | None |
### Example: printing context information to the log
@@ -184,10 +184,8 @@ The `github` context contains information about the workflow run and the event t
| `github.action_ref` | `string` | For a step executing an action, this is the ref of the action being executed. For example, `v2`.<br><br>{% data reusables.actions.composite-actions-unsupported-refs %} |
| `github.action_repository` | `string` | For a step executing an action, this is the owner and repository name of the action. For example, `actions/checkout`.<br><br>{% data reusables.actions.composite-actions-unsupported-refs %} |
| `github.action_status` | `string` | For a composite action, the current result of the composite action. |
| `github.actor` | `string` | {% ifversion actions-stable-actor-ids %}The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from `github.triggering_actor`. Any workflow re-runs will use the privileges of `github.actor`, even if the actor initiating the re-run (`github.triggering_actor`) has different privileges.{% else %}The username of the user that initiated the workflow run.{% endif %} |
{%- ifversion actions-oidc-custom-claims %}
| `github.actor` | `string` | The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from `github.triggering_actor`. Any workflow re-runs will use the privileges of `github.actor`, even if the actor initiating the re-run (`github.triggering_actor`) has different privileges. |
| `github.actor_id` | `string` | {% data reusables.actions.actor_id-description %} |
{%- endif %}
| `github.api_url` | `string` | The URL of the {% data variables.product.prodname_dotcom %} REST API. |
| `github.base_ref` | `string` | The `base_ref` or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either `pull_request` or `pull_request_target`. |
| `github.env` | `string` | Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "[AUTOTITLE](/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable)." |
@@ -203,13 +201,9 @@ The `github` context contains information about the workflow run and the event t
| `github.ref_protected` | `boolean` | {% data reusables.actions.ref_protected-description %} |
| `github.ref_type` | `string` | {% data reusables.actions.ref_type-description %} |
| `github.repository` | `string` | The owner and repository name. For example, `octocat/Hello-World`. |
{%- ifversion actions-oidc-custom-claims %}
| `github.repository_id` | `string` | {% data reusables.actions.repository_id-description %} |
{%- endif %}
| `github.repository_owner` | `string` | The repository owner's username. For example, `octocat`. |
{%- ifversion actions-oidc-custom-claims %}
| `github.repository_owner_id` | `string` | {% data reusables.actions.repository_owner_id-description %} |
{%- endif %}
| `github.repositoryUrl` | `string` | The Git URL to the repository. For example, `git://github.com/octocat/hello-world.git`. |
| `github.retention_days` | `string` | The number of days that workflow run logs and artifacts are kept. |
| `github.run_id` | `string` | {% data reusables.actions.run_id_description %} |
@@ -218,13 +212,11 @@ The `github` context contains information about the workflow run and the event t
| `github.secret_source` | `string` | The source of a secret used in a workflow. Possible values are `None`, `Actions`{% ifversion fpt or ghec %}, `Codespaces`{% endif %}, or `Dependabot`. |
| `github.server_url` | `string` | The URL of the GitHub server. For example: `https://github.com`. |
| `github.sha` | `string` | {% data reusables.actions.github_sha_description %} |
| `github.token` | `string` | A token to authenticate on behalf of the GitHub App installed on your repository. This is functionally equivalent to the `GITHUB_TOKEN` secret. For more information, see "[AUTOTITLE](/actions/security-guides/automatic-token-authentication)." <br /> Note: This context property is set by the Actions runner, and is only available within the execution `steps` of a job. Otherwise, the value of this property will be `null`. |{% ifversion actions-stable-actor-ids %}
| `github.triggering_actor` | `string` | {% data reusables.actions.github-triggering-actor-description %} |{% endif %}
| `github.token` | `string` | A token to authenticate on behalf of the GitHub App installed on your repository. This is functionally equivalent to the `GITHUB_TOKEN` secret. For more information, see "[AUTOTITLE](/actions/security-guides/automatic-token-authentication)." <br /> Note: This context property is set by the Actions runner, and is only available within the execution `steps` of a job. Otherwise, the value of this property will be `null`. |
| `github.triggering_actor` | `string` | {% data reusables.actions.github-triggering-actor-description %} |
| `github.workflow` | `string` | The name of the workflow. If the workflow file doesn't specify a `name`, the value of this property is the full path of the workflow file in the repository. |
{%- ifversion actions-oidc-custom-claims %}
| `github.workflow_ref` | `string` | {% data reusables.actions.workflow-ref-description %} |
| `github.workflow_sha` | `string` | {% data reusables.actions.workflow-sha-description %} |
{%- endif %}
| `github.workspace` | `string` | The default working directory on the runner for steps, and the default location of your repository when using the [`checkout`](https://github.com/actions/checkout) action. |
### Example contents of the `github` context
@@ -493,17 +485,10 @@ jobs:
output2: ${{ steps.step2.outputs.secondword }}
steps:
- id: step1{% endraw %}
{%- ifversion actions-save-state-set-output-envs %}
run: echo "firstword=hello" >> $GITHUB_OUTPUT
{%- else %}
run: echo "::set-output name=firstword::hello"
{%- endif %}{% raw %}
- id: step2{% endraw %}
{%- ifversion actions-save-state-set-output-envs %}
- id: step2
run: echo "secondword=world" >> $GITHUB_OUTPUT
{%- else %}
run: echo "::set-output name=secondword::world"
{%- endif %}{% raw %}
{% raw %}
```
{% endraw %}
@@ -554,11 +539,7 @@ jobs:
steps:
- name: Generate 0 or 1
id: generate_number
{%- ifversion actions-save-state-set-output-envs %}
run: echo "random_number=$(($RANDOM % 2))" >> $GITHUB_OUTPUT
{%- else %}
run: echo "::set-output name=random_number::$(($RANDOM % 2))"
{%- endif %}
- name: Pass or fail
run: |
if [[ {% raw %}${{ steps.generate_number.outputs.random_number }}{% endraw %} == 0 ]]; then exit 0; else exit 1; fi
@@ -572,18 +553,13 @@ The `runner` context contains information about the runner that is executing the
|---------------|------|-------------|
| `runner` | `object` | This context changes for each job in a workflow run. This object contains all the properties listed below. |
| `runner.name` | `string` | {% data reusables.actions.runner-name-description %} |
| `runner.os` | `string` | {% data reusables.actions.runner-os-description %} |{% ifversion actions-runner-arch-envvars %}
| `runner.arch` | `string` | {% data reusables.actions.runner-arch-description %} |{% endif %}
| `runner.os` | `string` | {% data reusables.actions.runner-os-description %} |
| `runner.arch` | `string` | {% data reusables.actions.runner-arch-description %} |
| `runner.temp` | `string` | {% data reusables.actions.runner-temp-directory-description %} |
| `runner.tool_cache` | `string` | {% data reusables.actions.runner-tool-cache-description %} |
| `runner.debug` | `string` | {% data reusables.actions.runner-debug-description %} |
| `runner.environment` | `string` | {% data reusables.actions.runner-environment-description %} |
{%- comment %}
The `runner.workspace` property is purposefully not documented. It is an early Actions property that now isn't relevant for users, compared to `github.workspace`. It is kept around for compatibility.
| `runner.workspace` | `string` | |
{%- endcomment %}
### Example contents of the `runner` context
The following example context is from a Linux {% data variables.product.prodname_dotcom %}-hosted runner.
@@ -799,11 +775,7 @@ jobs:
steps:
- name: Build
id: build_step
{%- ifversion actions-save-state-set-output-envs %}
run: echo "build_id=$RANDOM" >> $GITHUB_OUTPUT
{%- else %}
run: echo "::set-output name=build_id::$RANDOM"
{%- endif %}
deploy:
needs: build
runs-on: ubuntu-latest
@@ -819,13 +791,13 @@ jobs:
## `inputs` context
The `inputs` context contains input properties passed to an action{% ifversion actions-unified-inputs %},{% else %} or{% endif %} to a reusable workflow{% ifversion actions-unified-inputs %}, or to a manually triggered workflow{% endif %}. {% ifversion actions-unified-inputs %}For reusable workflows, the{% else %}The{% endif %} input names and types are defined in the [`workflow_call` event configuration](/actions/using-workflows/events-that-trigger-workflows#workflow-reuse-events) of a reusable workflow, and the input values are passed from [`jobs.<job_id>.with`](/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idwith) in an external workflow that calls the reusable workflow. {% ifversion actions-unified-inputs %}For manually triggered workflows, the inputs are defined in the [`workflow_dispatch` event configuration](/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch) of a workflow.{% endif %}
The `inputs` context contains input properties passed to an action, to a reusable workflow, or to a manually triggered workflow. For reusable workflows, the input names and types are defined in the [`workflow_call` event configuration](/actions/using-workflows/events-that-trigger-workflows#workflow-reuse-events) of a reusable workflow, and the input values are passed from [`jobs.<job_id>.with`](/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idwith) in an external workflow that calls the reusable workflow. For manually triggered workflows, the inputs are defined in the [`workflow_dispatch` event configuration](/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch) of a workflow.
The properties in the `inputs` context are defined in the workflow file. They are only available in a [reusable workflow](/actions/using-workflows/reusing-workflows){% ifversion actions-unified-inputs %} or in a workflow triggered by the [`workflow_dispatch` event](/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch){% endif %}
The properties in the `inputs` context are defined in the workflow file. They are only available in a [reusable workflow](/actions/using-workflows/reusing-workflows) or in a workflow triggered by the [`workflow_dispatch` event](/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch)
| Property name | Type | Description |
|---------------|------|-------------|
| `inputs` | `object` | This context is only available in a [reusable workflow](/actions/using-workflows/reusing-workflows){% ifversion actions-unified-inputs %} or in a workflow triggered by the [`workflow_dispatch` event](/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch){% endif %}. You can access this context from any job or step in a workflow. This object contains the properties listed below. |
| `inputs` | `object` | This context is only available in a [reusable workflow](/actions/using-workflows/reusing-workflows) or in a workflow triggered by the [`workflow_dispatch` event](/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch). You can access this context from any job or step in a workflow. This object contains the properties listed below. |
| `inputs.<name>` | `string` or `number` or `boolean` or `choice` | Each input value passed from an external workflow. |
### Example contents of the `inputs` context
@@ -872,8 +844,6 @@ jobs:
{% endraw %}
{% ifversion actions-unified-inputs %}
### Example usage of the `inputs` context in a manually triggered workflow
This example workflow triggered by a `workflow_dispatch` event uses the `inputs` context to get the values of the `build_id`, `deploy_target`, and `perform_deploy` inputs that were passed to the workflow.
@@ -904,4 +874,3 @@ jobs:
```
{% endraw %}
{% endif %}

View File

@@ -273,17 +273,15 @@ We strongly recommend that actions use variables to access the filesystem rather
| `GITHUB_ACTION` | The name of the action currently running, or the [`id`](/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsid) of a step. For example, for an action, `__repo-owner_name-of-action-repo`.<br><br>{% data variables.product.prodname_dotcom %} removes special characters, and uses the name `__run` when the current step runs a script without an `id`. If you use the same script or action more than once in the same job, the name will include a suffix that consists of the sequence number preceded by an underscore. For example, the first script you run will have the name `__run`, and the second script will be named `__run_2`. Similarly, the second invocation of `actions/checkout` will be `actionscheckout2`. |
| `GITHUB_ACTION_PATH` | The path where an action is located. This property is only supported in composite actions. You can use this path to change directories to where the action is located and access other files in that same repository. For example, `/home/runner/work/_actions/repo-owner/name-of-action-repo/v1`. |
| `GITHUB_ACTION_REPOSITORY` | For a step executing an action, this is the owner and repository name of the action. For example, `actions/checkout`. |
| `GITHUB_ACTIONS` | Always set to `true` when {% data variables.product.prodname_actions %} is running the workflow. You can use this variable to differentiate when tests are being run locally or by {% data variables.product.prodname_actions %}.
| `GITHUB_ACTIONS` | Always set to `true` when {% data variables.product.prodname_actions %} is running the workflow. You can use this variable to differentiate when tests are being run locally or by {% data variables.product.prodname_actions %}. |
| `GITHUB_ACTOR` | The name of the person or app that initiated the workflow. For example, `octocat`. |
{%- ifversion actions-oidc-custom-claims %}
| `GITHUB_ACTOR_ID` | {% data reusables.actions.actor_id-description %} |
{%- endif %}
| `GITHUB_API_URL` | Returns the API URL. For example: `{% data variables.product.rest_url %}`.
| `GITHUB_API_URL` | Returns the API URL. For example: `{% data variables.product.rest_url %}`. |
| `GITHUB_BASE_REF` | The name of the base ref or target branch of the pull request in a workflow run. This is only set when the event that triggers a workflow run is either `pull_request` or `pull_request_target`. For example, `main`. |
| `GITHUB_ENV` | The path on the runner to the file that sets variables from workflow commands. The path to this file is unique to the current step and changes for each step in a job. For example, `/home/runner/work/_temp/_runner_file_commands/set_env_87406d6e-4979-4d42-98e1-3dab1f48b13a`. For more information, see "[AUTOTITLE](/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable)." |
| `GITHUB_EVENT_NAME` | The name of the event that triggered the workflow. For example, `workflow_dispatch`. |
| `GITHUB_EVENT_PATH` | The path to the file on the runner that contains the full event webhook payload. For example, `/github/workflow/event.json`. |
| `GITHUB_GRAPHQL_URL` | Returns the GraphQL API URL. For example: `{% data variables.product.graphql_url %}`.
| `GITHUB_GRAPHQL_URL` | Returns the GraphQL API URL. For example: `{% data variables.product.graphql_url %}`. |
| `GITHUB_HEAD_REF` | The head ref or source branch of the pull request in a workflow run. This property is only set when the event that triggers a workflow run is either `pull_request` or `pull_request_target`. For example, `feature-branch-1`. |
| `GITHUB_JOB` | The [job_id](/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_id) of the current job. For example, `greeting_job`. |
| `GITHUB_OUTPUT` | The path on the runner to the file that sets the current step's outputs from workflow commands. The path to this file is unique to the current step and changes for each step in a job. For example, `/home/runner/work/_temp/_runner_file_commands/set_output_a50ef383-b063-46d9-9157-57953fc9f3f0`. For more information, see "[AUTOTITLE](/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter)." |
@@ -293,34 +291,22 @@ We strongly recommend that actions use variables to access the filesystem rather
| `GITHUB_REF_PROTECTED` | {% data reusables.actions.ref_protected-description %} |
| `GITHUB_REF_TYPE` | {% data reusables.actions.ref_type-description %} |
| `GITHUB_REPOSITORY` | The owner and repository name. For example, `octocat/Hello-World`. |
{%- ifversion actions-oidc-custom-claims %}
| `GITHUB_REPOSITORY_ID` | {% data reusables.actions.repository_id-description %} |
{%- endif %}
| `GITHUB_REPOSITORY_OWNER` | The repository owner's name. For example, `octocat`. |
{%- ifversion actions-oidc-custom-claims %}
| `GITHUB_REPOSITORY_OWNER_ID` | {% data reusables.actions.repository_owner_id-description %} |
{%- endif %}
| `GITHUB_RETENTION_DAYS` | The number of days that workflow run logs and artifacts are kept. For example, `90`. |
| `GITHUB_RUN_ATTEMPT` | A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run. For example, `3`. |
| `GITHUB_RUN_ID` | {% data reusables.actions.run_id_description %} For example, `1658821493`. |
| `GITHUB_RUN_NUMBER` | {% data reusables.actions.run_number_description %} For example, `3`. |
| `GITHUB_SERVER_URL`| The URL of the {% data variables.product.product_name %} server. For example: `https://{% data variables.product.product_url %}`.
| `GITHUB_SERVER_URL`| The URL of the {% data variables.product.product_name %} server. For example: `https://{% data variables.product.product_url %}`. |
| `GITHUB_SHA` | {% data reusables.actions.github_sha_description %} |
{%- ifversion actions-job-summaries %}
| `GITHUB_STEP_SUMMARY` | The path on the runner to the file that contains job summaries from workflow commands. The path to this file is unique to the current step and changes for each step in a job. For example, `/home/runner/_layout/_work/_temp/_runner_file_commands/step_summary_1cb22d7f-5663-41a8-9ffc-13472605c76c`. For more information, see "[AUTOTITLE](/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary)." |
{%- endif %}
{%- ifversion actions-stable-actor-ids %}
| `GITHUB_TRIGGERING_ACTOR` | {% data reusables.actions.github-triggering-actor-description %} |
{%- endif %}
| `GITHUB_WORKFLOW` | The name of the workflow. For example, `My test workflow`. If the workflow file doesn't specify a `name`, the value of this variable is the full path of the workflow file in the repository. |
{%- ifversion actions-oidc-custom-claims %}
| `GITHUB_WORKFLOW_REF` | {% data reusables.actions.workflow-ref-description %} |
| `GITHUB_WORKFLOW_SHA` | {% data reusables.actions.workflow-sha-description %} |
{%- endif %}
| `GITHUB_WORKSPACE` | The default working directory on the runner for steps, and the default location of your repository when using the [`checkout`](https://github.com/actions/checkout) action. For example, `/home/runner/work/my-repo-name/my-repo-name`. |
{%- ifversion actions-runner-arch-envvars %}
| `RUNNER_ARCH` | {% data reusables.actions.runner-arch-description %} |
{%- endif %}
| `RUNNER_DEBUG` | {% data reusables.actions.runner-debug-description %} |
| `RUNNER_NAME` | {% data reusables.actions.runner-name-description %} For example, `Hosted Agent` |
| `RUNNER_OS` | {% data reusables.actions.runner-os-description %} For example, `Windows` |

View File

@@ -56,7 +56,8 @@
"prevent-pushes-to-main": "node src/workflows/prevent-pushes-to-main.js",
"release-banner": "node src/ghes-releases/scripts/release-banner.js",
"remove-version-markup": "node src/ghes-releases/scripts/remove-version-markup.js",
"rendered-content-link-checker-cli": "node src/links/scripts/rendered-content-link-checker-cli.js",
"rendered-content-link-checker": "tsx src/links/scripts/rendered-content-link-checker.ts",
"rendered-content-link-checker-cli": "tsx src/links/scripts/rendered-content-link-checker-cli.ts",
"rest-dev": "node src/rest/scripts/update-files.js",
"show-action-deps": "echo 'Action Dependencies:' && rg '^[\\s|-]*(uses:.*)$' .github -I -N --no-heading -r '$1$2' | sort | uniq | cut -c 7-",
"start": "cross-env NODE_ENV=development ENABLED_LANGUAGES=en nodemon src/frame/server.ts",

View File

@@ -1,6 +1,7 @@
import path from 'path'
import fs from 'fs'
import type { Response } from 'express'
import walk from 'walk-sync'
import { zip, difference } from 'lodash-es'
import GithubSlugger from 'github-slugger'
@@ -10,10 +11,10 @@ import { beforeAll, describe, expect, test } from 'vitest'
import matter from '@/frame/lib/read-frontmatter.js'
import { renderContent } from '@/content-render/index.js'
import getApplicableVersions from '@/versions/lib/get-applicable-versions.js'
import contextualize from '@/frame/middleware/context/context.js'
import contextualize from '@/frame/middleware/context/context'
import shortVersions from '@/versions/middleware/short-versions.js'
import { ROOT } from '@/frame/lib/constants.js'
import type { Context, FrontmatterVersions } from '@/types'
import type { Context, ExtendedRequest, FrontmatterVersions } from '@/types'
const slugger = new GithubSlugger()
@@ -29,12 +30,6 @@ type Frontmatter = {
hidden?: boolean
}
type MockedRequest = {
language: string
pagePath: string
context: Context
}
function getFrontmatterData(markdown: string): Frontmatter {
const parsed = matter(markdown)
if (!parsed.data) throw new Error('No frontmatter')
@@ -139,13 +134,13 @@ describe.skip('category pages', () => {
const next = () => {}
const res = {}
const context: Context = {}
const req: MockedRequest = {
const req = {
language: 'en',
pagePath: '/en',
context,
}
await contextualize(req, res, next)
await contextualize(req as ExtendedRequest, res as Response, next)
await shortVersions(req, res, next)
// Save the index title for later testing

View File

@@ -1,5 +1,7 @@
import type { Page } from '@/types'
import contextualize from '@/frame/middleware/context/context.js'
import type { Response } from 'express'
import type { ExtendedRequest, Page } from '@/types'
import contextualize from '@/frame/middleware/context/context'
import features from '@/versions/middleware/features.js'
import shortVersions from '@/versions/middleware/short-versions.js'
@@ -63,7 +65,7 @@ export async function allDocuments(options: Options): Promise<AllDocument[]> {
context,
}
await contextualize(req, res, next)
await contextualize(req as ExtendedRequest, res as Response, next)
await shortVersions(req, res, next)
req.context.page = page
await features(req, res, next)

View File

@@ -7,7 +7,7 @@ import { program } from 'commander'
import fpt from '#src/versions/lib/non-enterprise-default-version.js'
import { allVersionKeys } from '#src/versions/lib/all-versions.js'
import { liquid } from '#src/content-render/index.js'
import contextualize from '#src/frame/middleware/context/context.js'
import contextualize from '#src/frame/middleware/context/context'
const layoutFilename = path.posix.join(process.cwd(), 'src/dev-toc/layout.html')
const layout = fs.readFileSync(layoutFilename, 'utf8')

View File

@@ -1,18 +1,22 @@
import languages from '#src/languages/lib/languages.js'
import enterpriseServerReleases from '#src/versions/lib/enterprise-server-releases.js'
import { allVersions } from '#src/versions/lib/all-versions.js'
import { productMap } from '#src/products/lib/all-products.js'
import type { NextFunction, Response } from 'express'
import type { ExtendedRequest, Context } from '@/types'
import languages from '@/languages/lib/languages.js'
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.js'
import { allVersions } from '@/versions/lib/all-versions.js'
import { productMap } from '@/products/lib/all-products.js'
import {
getVersionStringFromPath,
getProductStringFromPath,
getCategoryStringFromPath,
getPathWithoutLanguage,
getPathWithoutVersion,
} from '#src/frame/lib/path-utils.js'
import productNames from '#src/products/lib/product-names.js'
import warmServer from '#src/frame/lib/warm-server.js'
import nonEnterpriseDefaultVersion from '#src/versions/lib/non-enterprise-default-version.js'
import { getDataByLanguage, getUIDataMerged } from '#src/data-directory/lib/get-data.js'
} from '@/frame/lib/path-utils.js'
import productNames from '@/products/lib/product-names.js'
import warmServer from '@/frame/lib/warm-server.js'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version.js'
import { getDataByLanguage, getUIDataMerged } from '@/data-directory/lib/get-data.js'
// This doesn't change just because the request changes, so compute it once.
const enterpriseServerVersions = Object.keys(allVersions).filter((version) =>
@@ -21,18 +25,24 @@ const enterpriseServerVersions = Object.keys(allVersions).filter((version) =>
// Supply all route handlers with a baseline `req.context` object
// Note that additional middleware in middleware/index.js adds to this context object
export default async function contextualize(req, res, next) {
export default async function contextualize(
req: ExtendedRequest,
res: Response,
next: NextFunction,
) {
// Ensure that we load some data only once on first request
const { redirects, siteTree, pages: pageMap } = await warmServer()
const { redirects, siteTree, pages: pageMap } = await warmServer([])
req.context = {}
const context: Context = {}
req.context = context
req.context.process = { env: {} }
// define each context property explicitly for code-search friendliness
// e.g. searches for "req.context.page" will include results from this file
req.context.currentLanguage = req.language
req.context.userLanguage = req.userLanguage
req.context.currentVersion = getVersionStringFromPath(req.pagePath)
req.context.currentVersion = getVersionStringFromPath(req.pagePath) as string
req.context.currentVersionObj = allVersions[req.context.currentVersion]
req.context.currentProduct = getProductStringFromPath(req.pagePath)
req.context.currentCategory = getCategoryStringFromPath(req.pagePath)
@@ -79,8 +89,8 @@ export default async function contextualize(req, res, next) {
if (!page) {
throw new Error("The 'page' has not been put into the context yet.")
}
const enPath = context.currentPath.replace(`/${page.languageCode}`, '/en')
const enPage = context.pages[enPath]
const enPath = context.currentPath!.replace(`/${page.languageCode}`, '/en')
const enPage = context.pages![enPath]
if (!enPage) {
throw new Error(`Unable to find equivalent English page by the path '${enPath}'`)
}

View File

@@ -19,7 +19,7 @@ import handleErrors from '@/observability/middleware/handle-errors'
import handleNextDataPath from './handle-next-data-path'
import detectLanguage from '@/languages/middleware/detect-language'
import reloadTree from './reload-tree'
import context from './context/context.js'
import context from './context/context'
import shortVersions from '@/versions/middleware/short-versions.js'
import languageCodeRedirects from '@/redirects/middleware/language-code-redirects.js'
import handleRedirects from '@/redirects/middleware/handle-redirects.js'

View File

@@ -20,7 +20,7 @@ If the action finds any broken links, it opens an internal issue for the Docs Co
<pre>
curl -Lso /dev/null -w "%{http_code}\n" <em>URL</em>
</pre>
A `200` response is success.
A `200` response is success.
- You can see a comprehensive list of HTTP response codes [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
- For external links that now `404` or have otherwise gone missing entirely, you may be able to use the [Wayback Machine](https://web.archive.org) to see the page before it went offline.
@@ -39,4 +39,4 @@ Before you decide whether to exclude a link from the daily link checker, you sho
- Has it has been flagged as a broken link for more than a week, but the URL works when a real user opens it in their browser?
- Has the URL been available for more than 3 months? You can check using the [Wayback Machine](https://web.archive.org).
If you are confident that the URL for the article should work for real users, then you can open a pull request to add it to the `src/links/lib/excluded-links.js` file.
If you are confident that the URL for the article should work for real users, then you can open a pull request to add it to the `src/links/lib/excluded-links.ts` file.

View File

@@ -8,7 +8,9 @@
/* eslint-disable prefer-regex-literals */
export default [
type ExcludedLink = string | RegExp
const excludedLinks: ExcludedLink[] = [
// Skip GitHub search links.
// E.g. https://github.com/search?foo=bar
regex('https://github.com/search?'),
@@ -79,6 +81,8 @@ export default [
'https://packages.ubuntu.com/search?keywords=netcat&searchon=names',
]
export default excludedLinks
// Return a regular expression from a URL string that matches the URL
// as a base. It's basically shorthand for "URL.startsWith(BASE_URL)"
// but as a RegExp object.
@@ -88,6 +92,6 @@ export default [
// true
// > regex('https://github.com').test('otherhttps://github.com/page')
// false
function regex(url) {
function regex(url: string) {
return new RegExp('^' + url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
}

View File

@@ -1,14 +1,15 @@
import type { Response } from 'express'
import cheerio from 'cheerio'
import warmServer from '@/frame/lib/warm-server.js'
import { liquid } from '@/content-render/index.js'
import shortVersions from '@/versions/middleware/short-versions.js'
import contextualize from '@/frame/middleware/context/context.js'
import contextualize from '@/frame/middleware/context/context'
import features from '@/versions/middleware/features.js'
import findPage from '@/frame/middleware/find-page.js'
import { createMinimalProcessor } from '@/content-render/unified/processor.js'
import getRedirect from '@/redirects/lib/get-redirect.js'
import type { Page } from '@/types'
import type { ExtendedRequest, Page } from '@/types'
export type DocsUrls = {
[identifier: string]: string
@@ -116,7 +117,7 @@ async function renderInnerHTML(page: Page, permalink: Permalink) {
// Here it just exists for the sake of TypeScript.
context: {},
}
await contextualize(req, res, next)
await contextualize(req as ExtendedRequest, res as Response, next)
await shortVersions(req, res, next)
await findPage(req, res, next)
await features(req, res, next)

View File

@@ -7,21 +7,29 @@ import fs from 'fs'
import path from 'path'
import chalk from 'chalk'
import github from '#src/workflows/github.js'
import github from '@/workflows/github.js'
export type CoreInject = {
info: (message: string) => void
debug: (message: string) => void
warning: (message: string) => void
error: (message: string) => void
setOutput: (name: string, value: any) => void
setFailed: (message: string) => void
}
// Directs core logging to console
export function getCoreInject(debug) {
export function getCoreInject(debug: boolean): CoreInject {
return {
info: console.log,
debug: (message) => (debug ? console.warn(chalk.blue(message)) : {}),
warning: (message) => console.warn(chalk.yellow(message)),
debug: (message: string) => (debug ? console.warn(chalk.blue(message)) : {}),
warning: (message: string) => console.warn(chalk.yellow(message)),
error: console.error,
setOutput: (name, value) => {
setOutput: (name: string, value: any) => {
if (debug) {
console.log(`Output "${name}" set to: "${value}"`)
}
},
setFailed: (message) => {
setFailed: (message: string) => {
if (debug) {
console.log('setFailed called.')
}
@@ -36,8 +44,8 @@ const logsPath = path.join(cwd, '..', '..', 'logs')
if (!fs.existsSync(logsPath)) {
fs.mkdirSync(logsPath)
}
export function getUploadArtifactInject(debug) {
return (name, contents) => {
export function getUploadArtifactInject(debug: boolean) {
return (name: string, contents: string) => {
const logFilename = path.join(logsPath, `${new Date().toISOString().substr(0, 16)}-${name}`)
if (debug) {
fs.writeFileSync(logFilename, contents)

View File

@@ -10,10 +10,10 @@
import fs from 'fs'
import path from 'path'
import { program, Option, InvalidArgumentError } from 'commander'
import renderedContentLinkChecker from '#src/links/scripts/rendered-content-link-checker.js'
import { getCoreInject, getUploadArtifactInject } from '#src/links/scripts/action-injections.js'
import { allVersions } from '#src/versions/lib/all-versions.js'
import github from '#src/workflows/github.js'
import renderedContentLinkChecker from './rendered-content-link-checker'
import { getCoreInject, getUploadArtifactInject } from '@/links/scripts/action-injections.js'
import { allVersions } from '@/versions/lib/all-versions.js'
import github from '@/workflows/github.js'
const STATIC_PREFIXES = {
assets: path.resolve('assets'),
@@ -115,7 +115,7 @@ program
return resolvedPath
},
)
.arguments('[files...]', 'Specific files to check')
.arguments('[files...]')
.parse(process.argv)
const opts = program.opts()

View File

@@ -2,30 +2,85 @@
import fs from 'fs'
import path from 'path'
import cheerio from 'cheerio'
import cheerio, { type CheerioAPI, type Element } from 'cheerio'
import coreLib from '@actions/core'
import got, { RequestError } from 'got'
import chalk from 'chalk'
import { Low } from 'lowdb'
import { JSONFile } from 'lowdb/node'
import { JSONFilePreset } from 'lowdb/node'
import { type Octokit } from '@octokit/rest'
import type { Response } from 'express'
import shortVersions from '#src/versions/middleware/short-versions.js'
import contextualize from '#src/frame/middleware/context/context.js'
import features from '#src/versions/middleware/features.js'
import getRedirect from '#src/redirects/lib/get-redirect.js'
import warmServer from '#src/frame/lib/warm-server.js'
import { liquid } from '#src/content-render/index.js'
import { deprecated } from '#src/versions/lib/enterprise-server-releases.js'
import excludedLinks from '#src/links/lib/excluded-links.js'
import { getEnvInputs, boolEnvVar } from '#src/workflows/get-env-inputs.js'
import type { ExtendedRequest, Page, Permalink, Context } from '@/types'
import shortVersions from '@/versions/middleware/short-versions.js'
import contextualize from '@/frame/middleware/context/context'
import features from '@/versions/middleware/features.js'
import getRedirect from '@/redirects/lib/get-redirect.js'
import warmServer from '@/frame/lib/warm-server.js'
import { liquid } from '@/content-render/index.js'
import { deprecated } from '@/versions/lib/enterprise-server-releases.js'
import excludedLinks from '@/links/lib/excluded-links.js'
import { getEnvInputs, boolEnvVar } from '@/workflows/get-env-inputs.js'
import { debugTimeEnd, debugTimeStart } from './debug-time-taken.js'
import { uploadArtifact as uploadArtifactLib } from './upload-artifact.js'
import github from '#src/workflows/github.js'
import { getActionContext } from '#src/workflows/action-context.js'
import { createMinimalProcessor } from '#src/content-render/unified/processor.js'
import { createReportIssue, linkReports } from '#src/workflows/issue-report.js'
import github from '@/workflows/github.js'
import { getActionContext } from '@/workflows/action-context.js'
import { createMinimalProcessor } from '@/content-render/unified/processor.js'
import { createReportIssue, linkReports } from '@/workflows/issue-report.js'
import { type CoreInject } from '@/links/scripts/action-injections.js'
const STATIC_PREFIXES = {
type Flaw = {
WARNING?: string
CRITICAL?: string
isExternal?: boolean
}
type LinkFlaw = {
page: Page
permalink: Permalink
href?: string
url?: string
text?: string
src: string
flaw: Flaw
}
// type Core = CoreInject
type Redirects = Record<string, string>
type PageMap = Record<string, Page>
type UploadArtifact = (name: string, message: string) => void
type Options = {
level?: string
files?: string[]
random?: boolean
language?: string | string[]
filter?: string[]
version?: string | string[]
max?: number
linkReports?: boolean
actionUrl?: string
verbose?: boolean
checkExternalLinks?: boolean
createReport?: boolean
failOnFlaw?: boolean
shouldComment?: boolean
reportRepository?: string
reportAuthor?: string
reportLabel?: string
checkAnchors?: boolean
checkImages?: boolean
patient?: boolean
externalServerErrorsAsWarning?: string
verboseUrl?: string
bail?: boolean
commentLimitToExternalLinks?: boolean
actionContext?: any
}
const STATIC_PREFIXES: Record<string, string> = {
assets: path.resolve('assets'),
public: path.resolve(path.join('src', 'graphql', 'data')),
}
@@ -44,8 +99,22 @@ const EXTERNAL_LINK_CHECKER_MAX_AGE_MS =
const EXTERNAL_LINK_CHECKER_DB =
process.env.EXTERNAL_LINK_CHECKER_DB || 'external-link-checker-db.json'
const adapter = new JSONFile(EXTERNAL_LINK_CHECKER_DB)
const externalLinkCheckerDB = new Low(adapter, { urls: {} })
// const adapter = new JSONFile(EXTERNAL_LINK_CHECKER_DB)
type Data = {
urls: {
[url: string]: {
timestamp: number
result: {
ok: boolean
statusCode: number
}
}
}
}
const defaultData: Data = { urls: {} }
const externalLinkCheckerDB = await JSONFilePreset<Data>(EXTERNAL_LINK_CHECKER_DB, defaultData)
type DBType = typeof externalLinkCheckerDB
// Given a number and a percentage, return the same number with a *percentage*
// max change of making a bit larger or smaller.
@@ -54,7 +123,7 @@ const externalLinkCheckerDB = new Low(adapter, { urls: {} })
// numbers from the day it started which means that they don't ALL expire
// on the same day but start to expire in a bit of a "random pattern" so
// you don't get all or nothing.
function jitter(base, percentage) {
function jitter(base: number, percentage: number) {
const r = percentage / 100
const negative = Math.random() > 0.5 ? -1 : 1
return base + base * Math.random() * r * negative
@@ -65,11 +134,12 @@ function jitter(base, percentage) {
// check.
function linksToSkipFactory() {
const set = new Set(excludedLinks.filter((regexOrURL) => typeof regexOrURL === 'string'))
const regexes = excludedLinks.filter((regexOrURL) => regexOrURL instanceof RegExp)
return (href) => set.has(href) || regexes.some((regex) => regex.test(href))
// This `... as RegExp` because TypeScript can't (currently) understand the filtering.
const regexes = excludedLinks.filter((regexOrURL) => regexOrURL instanceof RegExp) as RegExp[]
return (href: string) => set.has(href) || regexes.some((regex) => regex.test(href))
}
const linksToSkip = linksToSkipFactory(excludedLinks)
const linksToSkip = linksToSkipFactory()
const CONTENT_ROOT = path.resolve('content')
@@ -105,7 +175,7 @@ if (import.meta.url.endsWith(process.argv[1])) {
}
}
const opts = {
const opts: Options = {
level: LEVEL,
files,
verbose: true,
@@ -134,7 +204,7 @@ if (import.meta.url.endsWith(process.argv[1])) {
getEnvInputs(['GITHUB_TOKEN'])
}
main(coreLib, octokit, uploadArtifactLib, opts, {})
main(coreLib, octokit, uploadArtifactLib, opts)
}
/*
@@ -173,7 +243,13 @@ if (import.meta.url.endsWith(process.argv[1])) {
* versions {Array<string>} - only certain pages' versions (e.g. )
*
*/
async function main(core, octokit, uploadArtifact, opts = {}) {
async function main(
core: any,
octokit: Octokit,
uploadArtifact: UploadArtifact,
opts: Options = {},
) {
const {
level = 'warning',
files = [],
@@ -201,7 +277,7 @@ async function main(core, octokit, uploadArtifact, opts = {}) {
// If we'd manually do the same operations that `warmServer()` does
// here (e.g. `loadPageMap()`), we'd end up having to do it all over
// again, the next time `contextualize()` is called.
const { redirects, pages: pageMap, pageList } = await warmServer()
const { redirects, pages: pageMap, pageList } = await warmServer([])
if (files.length) {
core.debug(`Limitting to files list: ${files.join(', ')}`)
@@ -265,7 +341,7 @@ async function main(core, octokit, uploadArtifact, opts = {}) {
debugTimeStart(core, 'processPages')
const t0 = new Date().getTime()
const flawsGroups = await Promise.all(
pages.map((page) =>
pages.map((page: Page) =>
processPage(core, page, pageMap, redirects, opts, externalLinkCheckerDB, versions),
),
)
@@ -288,7 +364,9 @@ async function main(core, octokit, uploadArtifact, opts = {}) {
const uniqueHrefs = new Set(flaws.map((flaw) => flaw.href))
if (flaws.length > 0) {
await uploadJsonFlawsArtifact(uploadArtifact, flaws, opts)
await uploadJsonFlawsArtifact(uploadArtifact, flaws, {
verboseUrl: opts.verboseUrl,
})
core.info(`All flaws written to artifact log.`)
if (createReport) {
core.info(`Creating issue for flaws...`)
@@ -344,7 +422,7 @@ async function main(core, octokit, uploadArtifact, opts = {}) {
}
}
async function commentOnPR(core, octokit, flaws, opts) {
async function commentOnPR(core: CoreInject, octokit: Octokit, flaws: LinkFlaw[], opts: Options) {
const { actionContext = {} } = opts
const { owner, repo } = actionContext
const pullNumber = actionContext?.pull_request?.number
@@ -364,7 +442,7 @@ async function commentOnPR(core, octokit, flaws, opts) {
})
let previousCommentId
for (const { body, id } of data) {
if (body.includes(findAgainSymbol)) {
if (body && body.includes(findAgainSymbol)) {
previousCommentId = id
}
}
@@ -412,12 +490,22 @@ async function commentOnPR(core, octokit, flaws, opts) {
}
}
function flawIssueDisplay(flaws, opts, mentionExternalExclusionList = true) {
function flawIssueDisplay(flaws: LinkFlaw[], opts: Options, mentionExternalExclusionList = true) {
let output = ''
let flawsToDisplay = 0
type LinkFlawWithPermalink = {
// page?: Page
// permalink?: Permalink
href?: string
url?: string
text?: string
src: string
flaw: Flaw
permalinkHrefs: string[]
}
// Group broken links for each page
const hrefsOnPageGroup = {}
const hrefsOnPageGroup: Record<string, Record<string, LinkFlawWithPermalink>> = {}
for (const { page, permalink, href, text, src, flaw } of flaws) {
// When we don't want to include external links in PR comments
if (opts.commentLimitToExternalLinks && !flaw.isExternal) {
@@ -473,7 +561,7 @@ function flawIssueDisplay(flaws, opts, mentionExternalExclusionList = true) {
if (mentionExternalExclusionList) {
output +=
'\n\n---\n\nIf any link reported in this issue is not actually broken ' +
'and repeatedly shows up on reports, consider making a PR that adds it as an exception to `src/links/lib/excluded-links.js`. ' +
'and repeatedly shows up on reports, consider making a PR that adds it as an exception to `src/links/lib/excluded-links.ts`. ' +
'For more information, see [Fixing broken links in GitHub user docs](https://github.com/github/docs/blob/main/src/links/lib/README.md).'
}
@@ -482,7 +570,7 @@ function flawIssueDisplay(flaws, opts, mentionExternalExclusionList = true) {
}links found in [this](${opts.actionUrl}) workflow.\n${output}`
}
function printGlobalCacheHitRatio(core) {
function printGlobalCacheHitRatio(core: CoreInject) {
const hits = globalCacheHitCount
const misses = globalCacheMissCount
// It could be that the files that were tested didn't have a single
@@ -498,9 +586,15 @@ function printGlobalCacheHitRatio(core) {
}
}
function getPages(pageList, languages, filters, files, max) {
function getPages(
pageList: Page[],
languages: string[],
filters: string[],
files: string[],
max: number | undefined,
) {
return pageList
.filter((page) => {
.filter((page: Page) => {
if (languages.length && !languages.includes(page.languageCode)) {
return false
}
@@ -537,7 +631,15 @@ function getPages(pageList, languages, filters, files, max) {
.slice(0, max ? Math.min(max, pageList.length) : pageList.length)
}
async function processPage(core, page, pageMap, redirects, opts, db, versions) {
async function processPage(
core: CoreInject,
page: Page,
pageMap: PageMap,
redirects: Redirects,
opts: Options,
db: DBType,
versions: string[],
) {
const { verbose, verboseUrl, bail } = opts
const allFlawsEach = await Promise.all(
page.permalinks
@@ -567,7 +669,15 @@ async function processPage(core, page, pageMap, redirects, opts, db, versions) {
return allFlaws
}
async function processPermalink(core, permalink, page, pageMap, redirects, opts, db) {
async function processPermalink(
core: any,
permalink: Permalink,
page: Page,
pageMap: PageMap,
redirects: Redirects,
opts: Options,
db: DBType,
) {
const {
level = 'critical',
checkAnchors,
@@ -587,12 +697,12 @@ async function processPermalink(core, permalink, page, pageMap, redirects, opts,
throw error
}
const $ = cheerio.load(html, { xmlMode: true })
const flaws = []
const links = []
const flaws: LinkFlaw[] = []
const links: Element[] = []
$('a[href]').each((i, link) => {
links.push(link)
})
const newFlaws = await Promise.all(
const newFlaws: LinkFlaw[] = await Promise.all(
links.map(async (link) => {
const { href } = link.attribs
@@ -615,7 +725,8 @@ async function processPermalink(core, permalink, page, pageMap, redirects, opts,
checkAnchors,
checkExternalLinks,
externalServerErrorsAsWarning,
{ verbose, patient, permalink },
permalink,
{ verbose, patient },
db,
)
@@ -657,7 +768,7 @@ async function processPermalink(core, permalink, page, pageMap, redirects, opts,
return globalImageSrcCheckCache.get(src)
}
const flaw = checkImageSrc(src, $)
const flaw = checkImageSrc(src)
globalImageSrcCheckCache.set(src, flaw)
@@ -674,12 +785,19 @@ async function processPermalink(core, permalink, page, pageMap, redirects, opts,
}
async function uploadJsonFlawsArtifact(
uploadArtifact,
flaws,
{ verboseUrl = null } = {},
uploadArtifact: UploadArtifact,
flaws: LinkFlaw[],
{ verboseUrl = null }: { verboseUrl?: string | null } = {},
artifactName = 'all-rendered-link-flaws.json',
) {
const printableFlaws = {}
type PrintableLinkFlaw = {
href?: string
url?: string
text?: string
src?: string
flaw?: Flaw
}
const printableFlaws: Record<string, PrintableLinkFlaw[]> = {}
for (const { page, permalink, href, text, src, flaw } of flaws) {
const fullPath = prettyFullPath(page.fullPath)
@@ -703,7 +821,11 @@ async function uploadJsonFlawsArtifact(
return uploadArtifact(artifactName, message)
}
function printFlaws(core, flaws, { verboseUrl = null } = {}) {
function printFlaws(
core: CoreInject,
flaws: LinkFlaw[],
{ verboseUrl }: { verboseUrl?: string | undefined } = {},
) {
let previousPage = null
let previousPermalink = null
@@ -743,7 +865,7 @@ function printFlaws(core, flaws, { verboseUrl = null } = {}) {
// `vi` or `ls` or `code` but if we display it relative to `cwd()` you
// can still paste it to the next command but it's not taking up so much
// space.
function prettyFullPath(fullPath) {
function prettyFullPath(fullPath: string) {
return path.relative(process.cwd(), fullPath)
}
@@ -753,17 +875,18 @@ let globalCacheHitCount = 0
let globalCacheMissCount = 0
async function checkHrefLink(
core,
href,
$,
redirects,
pageMap,
core: any,
href: string,
$: CheerioAPI,
redirects: Redirects,
pageMap: PageMap,
checkAnchors = false,
checkExternalLinks = false,
externalServerErrorsAsWarning = false,
{ verbose = false, patient = false, permalink } = {},
db = null,
) {
externalServerErrorsAsWarning: string | undefined | null = null,
permalink: Permalink,
{ verbose = false, patient = false }: { verbose?: boolean; patient?: boolean } = {},
db: DBType | null = null,
): Promise<Flaw | undefined> {
// this function handles hrefs in all the following forms:
// same article links:
@@ -860,9 +983,10 @@ async function checkHrefLink(
}
return { WARNING: 'Links with a trailing / will always redirect' }
} else {
if (pathname.split('/')[1] in STATIC_PREFIXES) {
const firstPart = pathname.split('/')[1]
if (STATIC_PREFIXES[firstPart]) {
const staticFilePath = path.join(
STATIC_PREFIXES[pathname.split('/')[1]],
STATIC_PREFIXES[firstPart],
pathname.split(path.sep).slice(2).join(path.sep),
)
if (!fs.existsSync(staticFilePath)) {
@@ -914,7 +1038,7 @@ async function checkHrefLink(
// simply try again later.
// However, an `ETIMEDOUT` means it could work but it didn't this time but
// might if we try again a different hour or day.
function isTemporaryRequestError(requestError) {
function isTemporaryRequestError(requestError: string | undefined) {
if (typeof requestError === 'string') {
// See https://betterstack.com/community/guides/scaling-nodejs/nodejs-errors/
// for a definition of each one.
@@ -927,7 +1051,12 @@ function isTemporaryRequestError(requestError) {
// Can't do this memoization within the checkExternalURL because it can
// return a Promise since it already collates multiple URLs under the
// same cache key.
async function checkExternalURLCached(core, href, { verbose, patient }, db) {
async function checkExternalURLCached(
core: CoreInject,
href: string,
{ verbose, patient }: { verbose?: boolean; patient?: boolean },
db: DBType | null,
) {
const cacheMaxAge = EXTERNAL_LINK_CHECKER_MAX_AGE_MS
const now = new Date().getTime()
const url = href.split('#')[0]
@@ -968,7 +1097,11 @@ async function checkExternalURLCached(core, href, { verbose, patient }, db) {
}
const _fetchCache = new Map()
async function checkExternalURL(core, url, { verbose = false, patient = false } = {}) {
async function checkExternalURL(
core: CoreInject,
url: string,
{ verbose = false, patient = false } = {},
) {
if (!url.startsWith('https://')) throw new Error('Invalid URL')
const cleanURL = url.split('#')[0]
if (!_fetchCache.has(cleanURL)) {
@@ -977,7 +1110,7 @@ async function checkExternalURL(core, url, { verbose = false, patient = false }
return _fetchCache.get(cleanURL)
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
// Global for recording which domains we get rate-limited on.
// For example, if you got rate limited on `something.github.com/foo`
@@ -985,7 +1118,11 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// it's good to know to now bother yet.
const _rateLimitedDomains = new Map()
async function innerFetch(core, url, config = {}) {
async function innerFetch(
core: CoreInject,
url: string,
config: { verbose?: boolean; useGET?: boolean; patient?: boolean; retries?: number } = {},
) {
const { verbose, useGET, patient } = config
const { hostname } = new URL(url)
@@ -1039,7 +1176,10 @@ async function innerFetch(core, url, config = {}) {
if (r.statusCode === 429) {
let sleepTime = Math.min(
60_000,
Math.max(10_000, getRetryAfterSleep(r.headers['retry-after'])),
Math.max(
10_000,
r.headers['retry-after'] ? getRetryAfterSleep(r.headers['retry-after']) : 1_000,
),
)
// Sprinkle a little jitter so it doesn't all start again all
// at the same time
@@ -1081,16 +1221,17 @@ async function innerFetch(core, url, config = {}) {
}
// Return number of milliseconds from a `Retry-After` header value
function getRetryAfterSleep(headerValue) {
function getRetryAfterSleep(headerValue: string) {
if (!headerValue) return 0
let ms = Math.round(parseFloat(headerValue) * 1000)
if (isNaN(ms)) {
ms = Math.max(0, new Date(headerValue) - new Date())
const nextDate = new Date(headerValue)
ms = Math.max(0, nextDate.getTime() - new Date().getTime())
}
return ms
}
function checkImageSrc(src, $) {
function checkImageSrc(src: string) {
if (!src.startsWith('/') && !src.startsWith('http')) {
return { CRITICAL: 'Image path is not absolute. Should start with a /' }
}
@@ -1115,7 +1256,7 @@ function checkImageSrc(src, $) {
}
}
function summarizeFlaws(core, flaws) {
function summarizeFlaws(core: CoreInject, flaws: LinkFlaw[]) {
if (flaws.length) {
core.info(
chalk.bold(
@@ -1127,7 +1268,7 @@ function summarizeFlaws(core, flaws) {
}
}
function summarizeCounts(core, pages, tookSeconds) {
function summarizeCounts(core: CoreInject, pages: Page[], tookSeconds: number) {
const count = pages.map((page) => page.permalinks.length).reduce((a, b) => a + b, 0)
core.info(
`Tested ${count.toLocaleString()} permalinks across ${pages.length.toLocaleString()} pages`,
@@ -1139,7 +1280,7 @@ function summarizeCounts(core, pages, tookSeconds) {
core.info(`~${pagesPerSecond.toFixed(1)} pages per second.`)
}
function shuffle(array) {
function shuffle(array: any[]) {
let currentIndex = array.length
let randomIndex
@@ -1156,19 +1297,21 @@ function shuffle(array) {
return array
}
async function renderInnerHTML(page, permalink) {
async function renderInnerHTML(page: Page, permalink: Permalink) {
const next = () => {}
const res = {}
const pagePath = permalink.href
const context: Context = {}
const req = {
path: pagePath,
language: permalink.languageCode,
pagePath,
cookies: {},
context,
}
// This will create and set `req.context = {...}`
await contextualize(req, res, next)
await contextualize(req as ExtendedRequest, res as Response, next)
await shortVersions(req, res, next)
req.context.page = page
await features(req, res, next)

View File

@@ -11,7 +11,7 @@ import {
makeLanguageSurrogateKey,
} from '@/frame/middleware/set-fastly-surrogate-key.js'
import shortVersions from '@/versions/middleware/short-versions.js'
import contextualize from '@/frame/middleware/context/context.js'
import contextualize from '@/frame/middleware/context/context'
import features from '@/versions/middleware/features.js'
import getRedirect from '@/redirects/lib/get-redirect.js'
import { isArchivedVersionByPath } from '@/archives/lib/is-archived-version.js'
@@ -130,7 +130,7 @@ export async function getPageInfo(page: Page, pathname: string) {
}
const next = () => {}
const res = {}
await contextualize(renderingReq, res, next)
await contextualize(renderingReq as ExtendedRequest, res as Response, next)
await shortVersions(renderingReq, res, next)
renderingReq.context.page = page
await features(renderingReq, res, next)

View File

@@ -1,6 +1,7 @@
import enterpriseServerReleases from '#src/versions/lib/enterprise-server-releases.js'
import type { ProductNames } from '@/types'
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.js'
const productNames = {
const productNames: ProductNames = {
dotcom: 'GitHub.com',
}

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest'
import productNames from '#src/products/lib/product-names.js'
import productNames from '@/products/lib/product-names'
describe('productNames module', () => {
test('is an object with product codes as keys and human-friendly names as values', () => {

View File

@@ -22,7 +22,7 @@ The results comprise the `page.redirects` object, whose keys are always only the
Sometimes it contains the specific plan/version (e.g. `/enterprise-server@3.0/v3/integrations` to `enterprise-server@3.0/developers/apps`) and sometimes it's just the plain path
(e.g. `/articles/viewing-your-repositorys-workflows` to `/actions/monitoring-and-troubleshooting-workflows`)
All of the above are merged into a global redirects object. This object gets added to `req.context` via `src/frame/middleware/context/context.js` and is made accessible on every request.
All of the above are merged into a global redirects object. This object gets added to `req.context` via `src/frame/middleware/context/context.ts` and is made accessible on every request.
In the `handle-redirects.js` middleware, the language part of the URL is
removed, looked up, and if matched to something, redirects with language

View File

@@ -1,5 +1,7 @@
import type { Request } from 'express'
import type enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.d.ts'
// Throughout our codebase we "extend" the Request object by attaching
// things to it. For example `req.context = { currentCategory: 'foo' }`.
// This type aims to match all the custom things we do to requests
@@ -12,15 +14,64 @@ export type ExtendedRequest = Request & {
// Add more properties here as needed
}
type Product = {
id: string
name: string
href: string
dir: string
toc: string
wip: boolean
hidden: boolean
versions: string[]
}
type ProductMap = {
[key: string]: Product
}
export type ProductNames = {
[shortName: string]: string
}
type Redirects = {
[key: string]: string
}
export type Context = {
currentCategory?: string
error?: Error
siteTree?: SiteTree
pages?: Record<string, Page>
redirects?: Record<string, Page>
productMap?: ProductMap
redirects?: Redirects
currentLanguage?: string
userLanguage?: string
currentPath?: string
allVersions?: AllVersions
currentPathWithoutLanguage?: string
currentArticle?: string
query?: Record<string, any>
relativePath?: string
page?: Page
enPage?: Page
productNames?: ProductNames
currentVersion?: string
process?: { env: {} }
site?: {
data: {
ui: any
}
}
currentVersionObj?: Version
currentProduct?: string
getEnglishPage?: (ctx: Context) => Page
getDottedData?: (dottedPath: string) => any
initialRestVersioningReleaseDate?: string
initialRestVersioningReleaseDateLong?: string
nonEnterpriseDefaultVersion?: string
enterpriseServerVersions?: string[]
enterpriseServerReleases?: typeof enterpriseServerReleases
languages?: Languages
}
type Language = {
@@ -56,6 +107,8 @@ export type Page = {
title: string
shortTitle?: string
intro: string
rawIntro?: string
rawPermissions?: string
languageCode: string
documentType: string
renderProp: (prop: string, context: any, opts: any) => Promise<string>
@@ -89,9 +142,33 @@ export type UnversionLanguageTree = {
export type Site = {
pages: Record<string, Page>
redirects: Record<string, string>
redirects: Redirects
unversionedTree: UnversionLanguageTree
siteTree: SiteTree
pageList: Page[]
pageMap: Record<string, Page>
}
export type Version = {
version: string
versionTitle: string
latestVersion: string
currentRelease: string
openApiVersionName: string
miscVersionName: string
apiVersions: string[]
latestApiVersion: string
plan: string
planTitle: string
shortName: string
releases: string[]
latestRelease: string
hasNumberedReleases: boolean
openApiBaseName: string
miscBaseName: string
nonEnterpriseDefault?: boolean
}
export type AllVersions = {
[name: string]: Version
}

13
src/versions/lib/all-versions.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import type { AllVersions } from '@/types'
export const allVersionKeys: string[]
export const allVersionShortnames: Record<string, string>
export declare function isApiVersioned(version: string): boolean
export declare function getDocsVersion(openApiVersion: string): string
export declare function getOpenApiVersion(version: string): string
export const allVersions: AllVersions

View File

@@ -0,0 +1,80 @@
type Dates = {
[key: string]: {
releaseDate: string
deprecationDate: string
}
}
export const dates: Dates
export const next: string
export const nextNext: string
export const supported: string[]
export const releaseCandidate: null | string
export const deprecatedWithFunctionalRedirects: string[]
export const deprecated: string[]
export const legacyAssetVersions: string[]
export const firstReleaseStoredInBlobStorage: string
export const all: string[]
export const latest: string
export const latestStable: string
export const oldestSupported: string
export const nextDeprecationDate: string
export const isOldestReleaseDeprecated: boolean
export const deprecatedOnNewSite: string[]
export const firstVersionDeprecatedOnNewSite: string
export const lastVersionWithoutArchivedRedirectsFile: string
export const lastReleaseWithLegacyFormat: string
export const deprecatedReleasesWithLegacyFormat: string[]
export const deprecatedReleasesWithNewFormat: string[]
export const deprecatedReleasesOnDeveloperSite: string[]
export const firstReleaseNote: string
export const firstRestoredAdminGuides: string
export declare function findReleaseNumberIndex(releaseNum: number): number
export declare function getNextReleaseNumber(releaseNum: number): string
export declare function getPreviousReleaseNumber(releaseNum: number): string
const allExports = {
dates,
next,
nextNext,
supported,
releaseCandidate,
deprecatedWithFunctionalRedirects,
deprecated,
legacyAssetVersions,
firstReleaseStoredInBlobStorage,
all,
latest,
latestStable,
oldestSupported,
nextDeprecationDate,
isOldestReleaseDeprecated,
deprecatedOnNewSite,
firstVersionDeprecatedOnNewSite,
lastVersionWithoutArchivedRedirectsFile,
lastReleaseWithLegacyFormat,
deprecatedReleasesWithLegacyFormat,
deprecatedReleasesWithNewFormat,
deprecatedReleasesOnDeveloperSite,
firstReleaseNote,
firstRestoredAdminGuides,
findReleaseNumberIndex,
getNextReleaseNumber,
getPreviousReleaseNumber,
}
export default allExports