Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci(docs-preview): Acquire PR context via gh CLI #4267

Merged
merged 3 commits into from
Nov 20, 2024

Conversation

polarathene
Copy link
Member

@polarathene polarathene commented Nov 18, 2024

Description

A pull_request + workflow_run solution that should work well with PRs from forks.

My preference is still to pull_request_target + workflow_call, but that is awaiting confirmation that it was implemented securely (EDIT: It is not secure, unless the untrusted code is only executed in an environment like a container).

UPDATE: Due to review feedback of #4264 (Solution C), while I do prefer that approach I am not comfortable moving forward with it for the project and will favor this PR (Solution B).

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change that does improve existing functionality)

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • New and existing unit tests pass locally with my changes
  • I have added information about changes made in this PR to CHANGELOG.md

@polarathene
Copy link
Member Author

polarathene commented Nov 19, 2024

TL;DR: Prefer Solution B (which this PR adapts to).


This is mostly for my benefit to document all of this research in one place along with full workflow configs to compare. It might also be a helpful resource to others weighing up which approach to implement on their projects.

These were all initially adapted from a community discussion started in 2021. I've since posted an answer there with a summary of the 3 solutions detailed below.

Reference - Solutions Comparison

To focus more on the comparing differences:

  • The bulk of commentary has been stripped away.
  • The build step has been simplified to just call build-docs.sh.

Each solution highlights some bullet points of differences. Beyond that they are effectively the same in functionality.

Should further context be needed, solutions B and C both link to a PR each which provides full commentary in the PR files source.


UPDATE: Solution C has been deemed high risk. I will keep it documented here, but heavily discourage it given the wider attack surface when the PR author can run untrusted code vs the reduced risk of pull_request (not only from restricted permissions + secrets, but branch isolation):

  • Cache poisoning allows uploading cache even with locked down permissions, which any other workflow on the same branch may then be compromised through. Do not allow pull_request_target to execute PR content without a manual approval mechanism. This can otherwise go undetected and compromise releases built from poisoned cache.
  • GITHUB_TOKEN and other secrets can be acquired via a memory dump. Coupled with cache poisoning to compromise other workflows, is where it gets more dangerous in impact.

Solution A - pull_request + workflow_run with ENV validation

This is the more common approach elsewhere as it's what Github demonstrates as a solution in Aug 2021.

The vulnerability to LD_PRELOAD from adding untrusted content into $GITHUB_ENV is avoided by preferring $GITHUB_OUTPUT instead or when viable validating the input (such as the value only being digits for a PR number).

More details with references: #4264 (comment)

No full workflow example here as solutions B and C resolve this better.

docs-preview-prepare.yml (partial)

Avoid storing key=value pairs into a single file here. Store only the value with the key as the file name, then construct the key=value entries in the later workflow_run workflow. This is less convenient but makes validation easier while also reducing risk.

      - name: 'Export PR Context'
        run: |
          mkdir pr-context
          echo '${{ github.event.pull_request.number }}' > ./pr-context/number

      - name: 'Upload context artifact for workflow transfer'
        uses: actions/upload-artifact@v4
        with:
          name: preview-build-context
          path: pr-context/
          retention-days: 1

docs-preview-deploy.yml (partial)

Verbosity will depend on how much validation you need to do, and if you use separate upload/download steps or combine via path with your build artifact (if your workflow has one).

  • I've demonstrated with using GITHUB_OUTPUT here and a separate job to match Solution B. You can reduce verbosity further with GITHUB_ENV with the steps all in a single job, which should be fine with the validation + explicit key=value in workflow_run.
  • An alternative when using GITHUB_OUTPUT is to not validate anything, storing key=value entries to a single file in the earlier pull_request workflow (where the PR author can add whatever they like). The workflow_run step would just append that files contents with cat pr-context.env >> "${GITHUB_OUTPUT}" which should be reasonably safe, but it would be better to validate or prefer solutions B or C instead. Do not do this with GITHUB_ENV, which would allow the PR author to inject ENV like LD_PRELOAD for an exploit to execute their own code in workflow_run.
jobs:
  # NOTE: This is handled as pre-requisite job to minimize the noise from acquiring these two outputs needed for `deploy-preview` ENV:
  pr-context:
    name: 'Acquire PR Context'
    runs-on: ubuntu-24.04
    outputs:
      PR_HEADSHA: ${{ steps.set-pr-context.outputs.head-sha }}
      PR_NUMBER:  ${{ steps.set-pr-context.outputs.number   }}
    # Skip this job (and thus the next job deploy-preview) if these conditions for `workflow_run` are not met:
    if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
    steps:
      - name: 'Retrieve and extract the pull_request context'
        uses: actions/download-artifact@v4
        with:
          name: preview-build-context
          path: pr-context/
          # These are needed due this approach relying on `workflow_run`, so that it can access the build artifact:
          # (uploaded from the associated `docs-preview-prepare.yml` workflow run)
          github-token: ${{ secrets.GITHUB_TOKEN }}
          run-id: ${{ github.event.workflow_run.id }}

      - name: 'Import PR Context'
        id: set-pr-context
        # Validate values loaded from files:
        # Bash pattern matching: https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html
        run: |
          # Hexadecimal value only:
          PR_HEADSHA=$(cat ./pr-context/head-sha)
          if ! [[ "${PR_HEADSHA}" =~ ^[[:xdigit:]]+$ ]]; then
            echo "Invalid SHA: ${PR_HEADSHA}"
            exit 1
          fi

          # Number only:
          PR_NUMBER=$(cat ./pr-context/number)
          if ! [[ "${PR_NUMBER}" =~ ^[[:digit:]]+$ ]]; then
            echo "Invalid PR number: ${PR_NUMBER}"
            exit 1
          fi

          # Append to GITHUB_ENV or GITHUB_OUTPUT:
          {
            echo "head-sha=${PR_HEADSHA}"
            echo "number=${PR_NUMBER}"
          } >> "${GITHUB_OUTPUT}"

  # For separate job above with `GITHUB_OUTPUT`, bring the outputs in as `env`:
  deploy-preview:
    name: 'Deploy Preview'
    runs-on: ubuntu-24.04
    needs: [pr-context]
    env:
      PR_HEADSHA: ${{ needs.pr-context.outputs.PR_HEADSHA }}
      PR_NUMBER:  ${{ needs.pr-context.outputs.PR_NUMBER  }}

Solution B - pull_request + workflow_run with gh pr view

#4267 (this PR)

  • actions/download-artifact must include the GH token and the run ID to retrieve the uploaded artifact from a separate workflow run (pull_request).
    • This isn't needed for pull_request_target + workflow_call which groups the two workflows under the same workflow run ID.
  • Adds the pr-context job to get:
    • The PR number (to support the PR comment functionality)
    • The PR commit head SHA (latest commit on PR) used in the PR comment and myrotvorets/set-commit-status-action.
    • NOTE: The head SHA appears to be the equivalent of ${{ github.event.workflow_run.head_sha }} (taken from context of the pull_request event workflow trigger?).
  • Adds myrotvorets/set-commit-status-action in workflow_run to manage the commit status (pending => success/failure), otherwise the PR is only aware of the build status from pull_request but not the success/failure of the actual deployment via workflow_run (other than the creation/update of the PR comment).

docs-preview-prepare.yml

name: 'Documentation (PR)'

on:
  pull_request:
    paths:
      - 'docs/**'
      - '.github/workflows/scripts/docs/build-docs.sh'
      - '.github/workflows/docs-preview-prepare.yml'

concurrency:
  group: deploypreview-pullrequest-${{ github.event.pull_request.number }}
  cancel-in-progress: true

env:
  BUILD_DIR: docs/site/
  PREVIEW_SITE_NAME: dms-doc-previews
  PREVIEW_SITE_PREFIX: pullrequest-${{ github.event.pull_request.number }}

permissions:
  # Required by `actions/checkout` for git checkout:
  contents: read

jobs:
  prepare-preview:
    name: 'Build Preview'
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4

      - name: 'Build with mkdocs-material via Docker'
        working-directory: docs/
        env:
          PREVIEW_URL: 'https://${{ env.PREVIEW_SITE_PREFIX }}--${{ env.PREVIEW_SITE_NAME }}.netlify.app/'
        run: bash ../.github/workflows/scripts/docs/build-docs.sh

      - name: 'Upload artifact for workflow transfer'
        uses: actions/upload-artifact@v4
        with:
          name: preview-build
          path: ${{ env.BUILD_DIR }}
          retention-days: 1

docs-preview-deploy.yml

name: 'Documentation (Deploy)'

on:
  workflow_run:
    workflows: ['Documentation (PR)']
    types:
      - completed

permissions:
  # Required by `actions/download-artifact`:
  actions: read
  # Required by `set-pr-context`:
  contents: read
  # Required by `marocchino/sticky-pull-request-comment` (write) + `set-pr-context` (read):
  pull-requests: write
  # Required by `myrotvorets/set-commit-status-action`:
  statuses: write

jobs:
  pr-context:
    name: 'Acquire PR Context'
    runs-on: ubuntu-24.04
    outputs:
      PR_HEADSHA: ${{ steps.set-pr-context.outputs.head-sha }}
      PR_NUMBER:  ${{ steps.set-pr-context.outputs.number   }}
    if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
    steps:
      - name: 'Get PR context'
        id: set-pr-context
        env:
          GH_TOKEN: ${{ github.token }}
          PR_TARGET_REPO: ${{ github.repository }}
          PR_BRANCH: |-
            ${{
              (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login)
                && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch)
                || github.event.workflow_run.head_branch
            }}
        run: |
          gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \
            --json 'number,headRefOid' \
            --jq '"number=\(.number)\nhead-sha=\(.headRefOid)"' \
            >> $GITHUB_OUTPUT

  deploy-preview:
    name: 'Deploy Preview'
    runs-on: ubuntu-24.04
    needs: [pr-context]
    env:
      BUILD_DIR: docs/site/
      PR_HEADSHA: ${{ needs.pr-context.outputs.PR_HEADSHA }}
      PR_NUMBER:  ${{ needs.pr-context.outputs.PR_NUMBER  }}
      PREVIEW_SITE_PREFIX: pullrequest-${{ needs.pr-context.outputs.PR_NUMBER }}
    steps:
      - name: 'Retrieve and extract the built docs preview'
        uses: actions/download-artifact@v4
        with:
          name: preview-build
          path: ${{ env.BUILD_DIR }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
          run-id: ${{ github.event.workflow_run.id }}

      # ==================== #
      # Deploy preview build #
      # ==================== #

      - name: 'Commit Status (1/2) - Set Workflow Status as Pending'
        uses: myrotvorets/set-commit-status-action@v2.0.1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          status: pending
          sha: ${{ env.PR_HEADSHA }}
          context: 'Deploy Preview (pull_request => workflow_run)'

      - name: 'Send preview build to Netlify'
        uses: nwtgck/actions-netlify@v3.0
        id: preview-netlify
        timeout-minutes: 1
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID:    ${{ secrets.NETLIFY_SITE_ID    }}
        with:
          fails-without-credentials: true
          alias: ${{ env.PREVIEW_SITE_PREFIX }}
          publish-dir: ${{ env.BUILD_DIR }}
          deploy-message: 'Preview Build (PR #${{ env.PR_NUMBER }} @ commit: ${{ env.PR_HEADSHA }}'
          # Disable unwanted action defaults:
          enable-commit-comment: false
          enable-commit-status: false
          enable-pull-request-comment: false
          enable-github-deployment: false

      - name: 'Comment on PR with preview link'
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          number: ${{ env.PR_NUMBER }}
          header: preview-comment
          recreate: true
          message: |
            [Documentation preview for this PR](${{ steps.preview-netlify.outputs.deploy-url }}) is ready! :tada:

            Built with commit: ${{ env.PR_HEADSHA }}

      - name: 'Commit Status (2/2) - Update deployment status'
        uses: myrotvorets/set-commit-status-action@v2.0.1
        if: ${{ always() }}
        env:
          DEPLOY_SUCCESS: Successfully deployed preview.
          DEPLOY_FAILURE: Failed to deploy preview.
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          status: ${{ job.status == 'success' && 'success' || 'failure' }}
          sha: ${{ env.PR_HEADSHA }}
          context: 'Deploy Preview (pull_request => workflow_run)'
          description: ${{ job.status == 'success' && env.DEPLOY_SUCCESS || env.DEPLOY_FAILURE }}

Solution C - pull_request_target + workflow_call

UPDATE: As per the PR link discussion and earlier warning for Solution C at the start of this reference, you are heavily discouraged from adopting this approach.

  • Understand the risks (known and unknown) and the impact that can have on the project and it's users should you adopt this solution.
  • Do not contribute this to a repository you are not willing to maintain and be responsible for.

#4264

  • docs-preview.yml is the new entrypoint workflow file with the pull_request_target trigger:
    • pull_request_target triggers the build and deploy workflows, instead of relying on workflow_run to implicitly trigger as a reaction to pull_request workflow completing, we have the more familiar job sequence/dependency with a single Actions workflow log (instead of the two separate ones from pull_request + workflow_run).
    • prepare (restricted permissions, no secrets) and deploy (trusted, like workflow_run) jobs reference re-usable workflows (workflow_call). These are effectively the same as their respective pull_request / workflow_run equivalents above.
    • prepare must never be given secrets from the calling workfow (docs-preview.yml), nor be granted write permissions, otherwise it will not match the pull_request environment. Any untrusted code that could be executed by the PR must only occur with docs-preview-prepare.yml, like it would with pull_request instead of workflow_call.
    • These prepare + deploy workflows have been adapted to receive inputs for context they'd need access to, including shared settings like BUILD_DIR. Due to current limitations with GH Actions, JSON is used with a pre-requisite step to provide access in a DRY manner.
  • actions/checkout:
    • Must reference the correct repo and branch for checkout of the PR branch. Required as the pull_request_target event is run from the base branch (target repo to merge PR into) instead of the head branch ref (the PR).
    • persist-credentials: false added as a precaution. Avoids keeping the token exposed on disk to future steps - but if the attacker can execute code/commands to dump memory, then they can retrieve the GH token and use it for whatever permissions were granted to the workflow/job.

Unlike pull_request, changes to any of these workflows cannot be run/tested when modified by the PR, as pull_request_target runs workflows from the PR base. It's also important to consider how this affects uses: <workflow ref>:

  • uses: docker-mailserver/docker-mailserver/.github/workflows/docs-preview-prepare.yml@master
    • Referencing a branch with @ can affect re-runs of workflows when the branch has been updated without requiring a rebase on the PR.
    • However, if the PR was not merging into that same branch of the target repo, it would also allow for the PR to run with the updated workflow when a rebase would not. This does have a risk of falling out of sync with docs-preview.yml, similar to how pull_request could fall out of sync with workflow_run (which only runs from the default branch of the repo).
  • uses: .github/workflows/docs-preview-prepare.yml
    • This would still run from the context of the common base SHA of your PR.

docs-preview.yml

name: 'Documentation (Preview)'

# For security reasons, it is necessary to split the workflow into two separate jobs to manage trust safely.

on:
  pull_request_target:
    paths:
      - 'docs/**'
      - '.github/workflows/scripts/docs/build-docs.sh'

concurrency:
  group: deploypreview-pullrequest-${{ github.event.pull_request.number }}
  cancel-in-progress: true

env:
  PREVIEW_CONTEXT: |
    {
      "build_dir": "docs/site/",
      "netlify": {
        "site_name":     "dms-doc-previews",
        "deploy_prefix": "pullrequest-${{ github.event.pull_request.number }}"
      },
      "pull_request": {
        "head_repo": "${{ github.event.pull_request.head.repo.full_name }}",
        "head_sha":  "${{ github.event.pull_request.head.sha }}",
        "number":    "${{ github.event.pull_request.number }}"
      }
    }

# This affects what permissions the `workflow_call` can be granted (they may only remove permissions needed):
# It cannot grant less than what those workflows require to run.
permissions:
  contents: read
  pull-requests: write

jobs:
  create-context:
    name: 'Create Context'
    runs-on: ubuntu-24.04
    outputs:
      preview-context: ${{ steps.set-preview-context.outputs.preview-context }}
    steps:
      - id: set-preview-context
        run: echo "preview-context=$(jq --compact-output <<< "${PREVIEW_CONTEXT}")" >> "${GITHUB_OUTPUT}"

  prepare:
    needs: [create-context]
    uses: .github/workflows/docs-preview-prepare.yml
    with:
      preview-context: ${{ needs.create-context.outputs.preview-context }}

  deploy:
    needs: [create-context, prepare]
    uses: .github/workflows/docs-preview-deploy.yml
    secrets:
      NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
      NETLIFY_SITE_ID:    ${{ secrets.NETLIFY_SITE_ID    }}
    with:
      preview-context: ${{ needs.create-context.outputs.preview-context }}

docs-preview-prepare.yml

name: 'Docs Preview (Build)'

on:
  workflow_call:
    inputs:
      preview-context:
        description: 'Preview Metadata (JSON)'
        required: true
        type: string

env:
  BUILD_DIR: ${{ fromJSON( inputs.preview-context ).build_dir }}
  PR_REF:  ${{ fromJSON( inputs.preview-context ).pull_request.head_sha  }}
  PR_REPO: ${{ fromJSON( inputs.preview-context ).pull_request.head_repo }}
  PREVIEW_SITE_NAME:   ${{ fromJSON( inputs.preview-context ).netlify.site_name     }}
  PREVIEW_SITE_PREFIX: ${{ fromJSON( inputs.preview-context ).netlify.deploy_prefix }}

permissions:
  # Required by `actions/checkout` for git checkout:
  contents: read

jobs:
  prepare-preview:
    name: 'Build Preview'
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ env.PR_REF }}
          repository: ${{ env.PR_REPO }}
          persist-credentials: false

      - name: 'Build with mkdocs-material via Docker'
        working-directory: docs/
        env:
          PREVIEW_URL: 'https://${{ env.PREVIEW_SITE_PREFIX }}--${{ env.PREVIEW_SITE_NAME }}.netlify.app/'
        run: bash ../.github/workflows/scripts/docs/build-docs.sh

      - name: 'Upload artifact for workflow transfer'
        uses: actions/upload-artifact@v4
        with:
          name: preview-build
          path: ${{ env.BUILD_DIR }}
          retention-days: 1

docs-preview-deploy.yml

name: 'Docs Preview (Deploy)'

on:
  workflow_call:
    inputs:
      preview-context:
        description: 'Preview Metadata (JSON)'
        required: true
        type: string
    secrets:
      NETLIFY_AUTH_TOKEN:
        required: true
      NETLIFY_SITE_ID:
        required: true

env:
  BUILD_DIR:  ${{ fromJSON( inputs.preview-context ).build_dir }}
  PR_HEADSHA: ${{ fromJSON( inputs.preview-context ).pull_request.head_sha }}
  PR_NUMBER:  ${{ fromJSON( inputs.preview-context ).pull_request.number }}
  PREVIEW_SITE_PREFIX: ${{ fromJSON( inputs.preview-context ).netlify.deploy_prefix }}

permissions:
  # Required by `marocchino/sticky-pull-request-comment`:
  pull-requests: write

jobs:
  deploy-preview:
    name: 'Deploy Preview'
    runs-on: ubuntu-24.04
    steps:
      - name: 'Retrieve and extract the built docs preview'
        uses: actions/download-artifact@v4
        with:
          name: preview-build
          path: ${{ env.BUILD_DIR }}

      # ==================== #
      # Deploy preview build #
      # ==================== #

      - name: 'Send preview build to Netlify'
        uses: nwtgck/actions-netlify@v3.0
        id: preview-netlify
        timeout-minutes: 1
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID:    ${{ secrets.NETLIFY_SITE_ID    }}
        with:
          fails-without-credentials: true
          alias: ${{ env.PREVIEW_SITE_PREFIX }}
          publish-dir: ${{ env.BUILD_DIR }}
          deploy-message: 'Preview Build (PR #${{ env.PR_NUMBER }} @ commit: ${{ env.PR_HEADSHA }}'
          # Disable unwanted action defaults:
          enable-commit-comment: false
          enable-commit-status: false
          enable-pull-request-comment: false
          enable-github-deployment: false

      - name: 'Comment on PR with preview link'
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          number: ${{ env.PR_NUMBER }}
          header: preview-comment
          recreate: true
          message: |
            [Documentation preview for this PR](${{ steps.preview-netlify.outputs.deploy-url }}) is ready! :tada:

            Built with commit: ${{ env.PR_HEADSHA }}

@davidlj95
Copy link

This is mostly for my benefit to document all of this research in one place along with full workflow configs to compare. It might also be a helpful resource to others weighing up which approach to implement on their projects.

Thanks a lot for this!! ⭐ Really helpful

Just a small correction: there are two Solution B , I can't C see the C one 😛

@polarathene
Copy link
Member Author

Thanks a lot for this!! ⭐ Really helpful

Glad to hear that 🎉

Just a small correction: there are two Solution B , I can't C see the C one 😛

Whoops! Thanks for pointing that out, I've corrected it 😅

casperklein
casperklein previously approved these changes Nov 19, 2024
@polarathene polarathene self-assigned this Nov 20, 2024
@polarathene polarathene added area/ci kind/improvement Improve an existing feature, configuration file or the documentation kind/bug/fix A fix (PR) for a confirmed bug labels Nov 20, 2024
@polarathene polarathene added this to the v15.0.0 milestone Nov 20, 2024
@polarathene polarathene merged commit 02f1894 into master Nov 20, 2024
1 check passed
@polarathene polarathene deleted the ci/docs-preview-use-gh-cli branch November 20, 2024 03:37
# Required by `marocchino/sticky-pull-request-comment`:
# Required by `set-pr-context`:
contents: read
# Required by `marocchino/sticky-pull-request-comment` (write) + `set-pr-context` (read):
pull-requests: write

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to move the write permissions to the job that needs them (deploy-preview)

&& github.event.workflow_run.event == 'pull_request'
&& contains(github.event.workflow_run.pull_requests.*.head.sha, github.event.workflow_run.head_sha)
PR_HEADSHA: ${{ steps.set-pr-context.outputs.head-sha }}
PR_NUMBER: ${{ steps.set-pr-context.outputs.number }}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PR_NUMBER: ${{ steps.set-pr-context.outputs.number }}
PR_NUMBER: ${{ steps.set-pr-context.outputs.number }}

jerith666 added a commit to jerith666/elbum that referenced this pull request Jan 12, 2025
jerith666 added a commit to jerith666/elbum that referenced this pull request Jan 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/ci kind/bug/fix A fix (PR) for a confirmed bug kind/improvement Improve an existing feature, configuration file or the documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants