4
.github/workflows/link-check-daily.yml
vendored
4
.github/workflows/link-check-daily.yml
vendored
@@ -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: |
|
||||
|
||||
2
.github/workflows/link-check-on-pr.yml
vendored
2
.github/workflows/link-check-on-pr.yml
vendored
@@ -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
|
||||
|
||||
@@ -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.<job_id>.concurrency</code> | <code>github, needs, strategy, matrix, inputs, vars</code> | None |
|
||||
| <code>jobs.<job_id>.container</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.container.credentials</code> | <code>github, needs, strategy, matrix, env, vars, secrets, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.container.env.<env_id></code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.container.image</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.continue-on-error</code> | <code>github, needs, strategy, vars, matrix, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.defaults.run</code> | <code>github, needs, strategy, matrix, env, vars, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.env</code> | <code>github, needs, strategy, matrix, vars, secrets, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.environment</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.environment.url</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, steps, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.if</code> | <code>github, needs, vars, inputs</code> | <code>always, cancelled, success, failure</code> |
|
||||
| <code>jobs.<job_id>.name</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.outputs.<output_id></code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.runs-on</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.secrets.<secrets_id></code> | <code>github, needs,{% ifversion actions-reusable-workflow-matrix %} strategy, matrix,{% endif %} secrets{% ifversion actions-unified-inputs %}, inputs{% endif %}, vars</code> | None |
|
||||
| <code>jobs.<job_id>.services</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.services.<service_id>.credentials</code> | <code>github, needs, strategy, matrix, env, vars, secrets, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.services.<service_id>.env.<env_id></code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.steps.continue-on-error</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
|
||||
| <code>jobs.<job_id>.steps.env</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
|
||||
| <code>jobs.<job_id>.steps.if</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, steps, inputs</code> | <code>always, cancelled, success, failure, hashFiles</code> |
|
||||
| <code>jobs.<job_id>.steps.name</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
|
||||
| <code>jobs.<job_id>.steps.run</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
|
||||
| <code>jobs.<job_id>.steps.timeout-minutes</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
|
||||
| <code>jobs.<job_id>.steps.with</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
|
||||
| <code>jobs.<job_id>.steps.working-directory</code> | <code>github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs</code> | <code>hashFiles</code> |
|
||||
| <code>jobs.<job_id>.strategy</code> | <code>github, needs, vars, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.timeout-minutes</code> | <code>github, needs, strategy, matrix, vars, inputs</code> | None |
|
||||
| <code>jobs.<job_id>.with.<with_id></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.<inputs_id>.default</code> | <code>github{% ifversion actions-unified-inputs %}, inputs{% endif %}, vars</code> | None |
|
||||
| <code>on.workflow_call.outputs.<output_id>.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 %}
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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}'`)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, '\\$&'))
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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
|
||||
|
||||
81
src/types.ts
81
src/types.ts
@@ -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
13
src/versions/lib/all-versions.d.ts
vendored
Normal 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
|
||||
80
src/versions/lib/enterprise-server-releases.d.ts
vendored
Normal file
80
src/versions/lib/enterprise-server-releases.d.ts
vendored
Normal 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
|
||||
Reference in New Issue
Block a user