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

docs: preview in PRs from forks #194

Open
AlexanderLanin opened this issue Jan 20, 2025 · 17 comments
Open

docs: preview in PRs from forks #194

AlexanderLanin opened this issue Jan 20, 2025 · 17 comments
Assignees
Labels
docs-as-code infrastructure General Score infrastructure topics

Comments

@AlexanderLanin
Copy link
Member

AlexanderLanin commented Jan 20, 2025

Problem Description
Currently PRs from forks do not have any kind of preview for the generated documentation.

The workflow is established, but it has insufficient access rights when the PR stems from a fork. Detailed explanation and potential solution at eclipse-score/.eclipsefdn#11

Task:

  • Understand the solution
  • Either break down the solution into sub-issues or implement it, depending on required effort
@AlexanderLanin AlexanderLanin added infrastructure General Score infrastructure topics docs-as-code labels Jan 21, 2025
@AlexanderLanin AlexanderLanin changed the title docs: preview in PRs docs: preview in PRs from forks Jan 28, 2025
@dcalavrezo-qorix dcalavrezo-qorix self-assigned this Jan 29, 2025
@dcalavrezo-qorix dcalavrezo-qorix moved this from Todo to In Progress in Operational (Tooling/Infrastructure) Jan 31, 2025
@dcalavrezo-qorix
Copy link
Contributor

dcalavrezo-qorix commented Jan 31, 2025

Apparently this is by design to prevent security risks from malicious PRs.

  • When a PR comes from a fork, GitHub blocks GITHUB_TOKEN from writing to protected branches (like gh-pages).
  • Even with pull_request_target, GitHub restricts direct pushes to the same repository (eclipse-score/score).

A safe alternative would be to create a separate repository - something like score-previews where only the previews from PR are pushed.
Then the requests would be served from another URL, like https://eclipse-score.github.io/score-previews

The https://eclipse-score.github.io/score would only hold main and releases.

This would avoid cluttering the gh-pages ( although in theory they are deleted when the PR is merged).

Nevertheless, we could still try the following:

  1. Use pull_request_target instead of pull_request, which (theoretically - there are some contradictions - see above) runs in the context of the base repository, allowing it to get write permissions.
  2. Create a DEPLOY_PREVIEW_TOKEN which we could store in the score secrets
    This can be passed as a parameter to the github-pages-deploy-action

Image

@dcalavrezo-qorix
Copy link
Contributor

Fine grained PATs do not allow branch restrictions sadly. So we could in theory create a DEPLOY_PREVIEW_TOKEN, but GH doesn't allow for it to be restricted to a specific branch ( gh-pages for us)

@dcalavrezo-qorix
Copy link
Contributor

dcalavrezo-qorix commented Jan 31, 2025

The github-pages-deploy-action also allows ssh-key usage

Image

So, one could:

  1. generate a ssh-key
ssh-keygen -t ed25519 -C "GitHub Pages Actions Deploy Key" -f gh-pages-deploy-key

2, add the public key to GH

  • in the repo → Settings → Deploy Keys.
  • Click Add deploy key.
  • Title: gh-pages Deploy Key
  • Paste the contents of gh-pages-deploy-key.pub.
  • Check "Allow write access"
  • Save.
  1. store the private key as a secret
  • In the repo -> go to -> Settings → Secrets and variables → Actions.
  • Click New repository secret.
  • Name: DEPLOY_PREVIEW_SSH_KEY
  • Value: Paste the private key from gh-pages-deploy-key.
  • Save.
  1. modify the GH action
${{ secrets.DEPLOY_PREVIEW_SSH_KEY }}

@AlexanderLanin
Copy link
Member Author

Is an Environment secret also a viable alternative? With review by "trusted contributors" before the action starts. Just collecting ideas.

@dcalavrezo-qorix
Copy link
Contributor

@AlexanderLanin yes, using a combination of environment secrets with manual approval is definitely an alternative, which would require a trusteed reviewer ( not a big problem from my pov, considering the PRs have to be reviewed in any case).

So the steps would be to store a GH Actions secret in an Env and require manual approval before the workflow can access the secret. Afterwards, the secret will be used to authenticate and push changes to the gh-pages branch.

Steps

Create an env with a secret


  1. Navigate to Settings → Environments.
  2. Click New environment, name it (e.g., gh-pages-deploy).
  3. Under Required reviewers, add trusted maintainers who can approve deployments.
  4. Add a secret (e.g., GH_PAGES_DEPLOY_TOKEN) containing a GitHub Personal Access Token (PAT) or a deploy key with write permissions - check the previous comments

Modify Your GitHub Actions Workflow


The documentation workflow should be modified to:

-Use pull_request_target to ensure it runs with main repository permissions.

name: Documentation
on:
  pull_request_target:  # Allows the workflow to run with repo permissions
    types: [opened, reopened, synchronize]
  push:
  merge_group:
    types: [checks_requested]
  • Wait for manual approval before accessing the secret.
  • Authenticate using the secret to push to gh-pages.

@dcalavrezo-qorix
Copy link
Contributor

dcalavrezo-qorix commented Feb 3, 2025

I will test the behavior on some dummy repos to see how these work

@dcalavrezo-qorix
Copy link
Contributor

Since pull_request_target runs in the context of the base repository, it does not automatically check out the PR's code. This is why no changes are detected and it is not a viable option.

@dcalavrezo-qorix
Copy link
Contributor

Tried with a PAT (configured as secret).
Even though the environment requires approval, GitHub does not allow secrets from repository settings to be used in workflows triggered by forks for security reasons.

That's why my secret is never available in forked PRs, even after approval.

@dcalavrezo-qorix
Copy link
Contributor

dcalavrezo-qorix commented Feb 3, 2025

Tried also configuring a separate env, which required manual approval and triggering. Sadly the secrets are still obfuscated by GitHub for the PRs coming from forks.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: gh-pages-deploy
    permissions:
        contents: write  # ✅ Required for pushing to gh-pages
        pages: write
        id-token: write    
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name:  Check If Token is Passed
        run: |
          if [ -z "${{ secrets.GH_PAGES_DEPLOY_TOKEN }}" ]; then
            echo "❌ ERROR: GH_PAGES_DEPLOY_TOKEN is empty!"
            exit 1
          else
            echo "✅ GH_PAGES_DEPLOY_TOKEN is available."
          fi        

      - name: Run Custom Deploy Action 🚀
        uses: ./.github/actions/deploy-versioned-pages
        with:
          source_folder: "docs/build"  # Change to an actual test folder
          versions_file: "versions"
          create_comment: "true"
          gh_token: ${{ secrets.GH_PAGES_DEPLOY_TOKEN }}

@dcalavrezo-qorix
Copy link
Contributor

Both env variables and secrets are not accessible from forked PRs.
Tried creating a separate repo as well. That didn't work either, meaning it only works if we don't ensure any special branch protection rule for that repo , nor do we require PAT ok SSH keys to deploy ( which I guess it would make it more vulnerable) - but it is just for PR previews

Also, trying to embed a private key in the workflow doens't do the trick, as GH protection pick-up on it

remote: error: GH013: Repository rule violations found for refs/heads/test3.
remote:
remote: - GITHUB PUSH PROTECTION
remote:   —————————————————————————————————————————
remote:     Resolve the following violations before pushing again
remote:
remote:     - Push cannot contain secrets
remote:
remote:
remote:      (?) Learn how to resolve a blocked push
remote:      https://docs.github.com/code-security/secret-scanning/working-with-secret-scanning-and-push-protection/working-with-push-protection-from-the-command-line#resolving-a-blocked-push
remote:
remote:      (?) This repository does not have Secret Scanning enabled, but is eligible. Enable Secret Scanning to view and manage detected secrets.
remote:      Visit the repository settings page, https://github.com/dcalavrezo-qorix/gh-page-test/settings/security_analysis
remote:
remote:
remote:       —— GitHub SSH Private Key ————————————————————————————
remote:        locations:
remote:          - commit: a590724c217df2550f2f95202126de5348736e19
remote:            path: .github/workflows/test-deployment.yml:41
remote:
remote:        (?) To push, remove secret from commit(s) or follow this URL to allow the secret.
remote:        https://github.com/dcalavrezo-qorix/gh-page-test/security/secret-scanning/unblock-secret/2sX4Hek2pABKzrhpvcPSmQBvGxE
remote:
remote:
remote:
To github.com:dcalavrezo-qorix/gh-page-test.git

@dcalavrezo-qorix
Copy link
Contributor

dcalavrezo-qorix commented Feb 3, 2025

I attempted to use a workflow_run that triggers upon the completion of a doc build workflow to publish documentation. The doc build workflow successfully uploads an artifact containing the documentation.

However, the workflow_run (which would run in the context of the source repo, and not the fork) fails to access this artifact because GitHub Actions does not support sharing artifacts between workflows, even if they belong to the same repository.

Artifacts are scoped to the workflow that created them and workflow_run workflows cannot directly access them. This limitation makes the workflow_run approach unsuitable for artifact-dependent tasks across workflows.

The idea was that when a PR (from a fork was triggered)

name: Test Deploy Versioned Pages

on:
  pull_request:
    types: [opened, synchronize, closed]
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: write
  pages: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
        contents: write  # ✅ Required for pushing to gh-pages
        pages: write
        id-token: write    
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
        

      - name: Verify docs/build Folder
        run: |
          if [ ! -d "docs/build" ]; then
            echo "❌ The folder docs/build does not exist!"
            exit 1
          fi
 
      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: docs-build
          path: docs/build/
          retention-days: 1  

the artifact is uploaded and there is a workflow_run that executes in the context of the source repo (not the forked one) and publishes the GH Pages

name: Deploy Documentation to gh-pages

on:
  workflow_run:
    workflows: ["Test Deploy Versioned Pages"]  
    types:
      - completed
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write  
      pages: write
      id-token: write

    steps:
      # Step 1: Check out the deployment repository
      - name: Checkout Deployment Repository
        uses: actions/checkout@v4


      - name: Download Artifact
        uses: actions/download-artifact@v4
        with:
          name: docs-build 
          repository: ${{ github.repository }}
          run-id: ${{ github.event.workflow_run.id }}


      - name: Run Custom Deploy Action 🚀
        uses: ./.github/actions/deploy-versioned-pages
        with:
          event_name: ${{ github.event_name }}
          ref_name: ${{ github.ref_name }}
          pr_number: ${{ github.event.pull_request.number || '' }}
          source_folder: "docs-build"  
          versions_file: "versions"
          create_comment: "true"

@dcalavrezo-qorix
Copy link
Contributor

After exploring multiple approaches, including workflow_run triggers and artifact sharing between workflows, the only viable solution to publish GitHub Pages from pull requests originating from forks is to use a separate repository (score-preview) dedicated to hosting preview pages.
This repository does not enforce branch protection rules, allowing workflows to push changes directly. However, this setup introduces potential risks, such as susceptibility to malicious attacks, since contributors from forks could exploit this workflow to publish unauthorized changes.

To mitigate these risks:

  • Restrict write access to this separate repository (**score-preview **) to workflows only.
  • Monitor activity in the preview repository regularly for any unexpected behavior.

For official releases and the primary documentation, the original repository ( score) will continue to host and manage them with branch protection and other safeguards in place. This ensures the integrity of the main documentation while enabling a workaround for PR previews from forks.

@dcalavrezo-qorix
Copy link
Contributor

Went over the documentation again https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token

It appears that pull_request_target is indeed the way to go.

The problem with my earlier attempts was that we need to manually checkout the code from the forked branch/PR.

@dcalavrezo-qorix
Copy link
Contributor

dcalavrezo-qorix commented Feb 5, 2025

Pushing to GitHub Pages from a Forked Pull Request (conclusions)

When working with GitHub Pages in repositories where contributors submit changes via forks, special considerations must be taken due to security restrictions imposed by GitHub Actions. This comment covers how to safely publish content to GitHub Pages from a pull request originating from a fork, based on previous research and try-outs on dummy repos.

Why Use pull_request_target Instead of pull_request?

By default, GitHub Actions run with minimal permissions when triggered by a pull_request event from a fork. This means that workflows cannot access repository secrets, preventing them from deploying changes to GitHub Pages.

To overcome this, we can use pull_request_target instead of pull_request. The pull_request_target event runs workflows in the context of the base repository rather than the fork, allowing access to repository secrets. However, since this introduces potential security risks (such as arbitrary code execution with write access), additional safeguards must be implemented.

Implementing Security Measures

To safely allow a workflow to push to GitHub Pages, the following measures should be taken:

1. Require Maintainer Approval Before Running Workflows

To prevent unauthorized code execution, an environment with required approvals should be used. This ensures that workflows triggered by pull_request_target do not automatically execute until approved by a maintainer.

In GitHub, navigate to Settings > Environments, create an environment (e.g., pages-deploy) and configure it to require manual approval.

We can modify the workflow to include the environment key:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: pages-deploy
    steps:
      - name: Checkout repository (Handle all events)
        uses: actions/checkout@v4.2.2
        with:
          ref: ${{ github.head_ref || github.event.pull_request.head.ref || github.ref }}
          repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}

With this setup, GitHub will pause execution of the workflow until a maintainer approves it.

2. Checking User Permissions

To prevent unauthorized users from triggering the deployment, we can add an additional permission check using the actions-cool/check-user-permission action.

The following step can be added to the Documentation workflow:

- name: Get User Permission
  id: checkAccess
  uses: actions-cool/check-user-permission@v2
  with:
    require: write
    username: ${{ github.triggering_actor }}
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Check User Permission
  if: steps.checkAccess.outputs.require-result == 'false'
  run: |
    echo "${{ github.triggering_actor }} does not have permissions on this repo."
    echo "Current permission level is ${{ steps.checkAccess.outputs.user-permission }}"
    echo "Job originally triggered by ${{ github.actor }}"
    exit 1

This step ensures that only users with at least write permissions can proceed with the deployment.

3. Handling pull_request_target Workflow Adaptations

Since pull_request_target runs workflows in the context of the base repository rather than the source fork, special considerations must be taken when checking out the repository. The following checkout step ensures that the workflow correctly fetches the pull request source:

- name: Checkout repository (Handle all events)
  uses: actions/checkout@v4.2.2
  with:
    ref: ${{ github.head_ref || github.event.pull_request.head.ref || github.ref }}
    repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}

4. Publishing to a Separate Repository

If direct deployment from forks poses too many security concerns, an alternative approach is to push generated content to a separate repository (e.g., score/score-previews). This repository can be configured to serve GitHub Pages, avoiding the need to grant write access to the main repository.

Other considerations

1. Deploying GitHub Pages Using JamesIves/github-pages-deploy-action

For deploying to GitHub Pages, we use the JamesIves/github-pages-deploy-action@v4. This allows deploying specific folders while maintaining repository cleanliness.

Example usage for main deployment:

- name: Deploy Documentation
  uses: JamesIves/github-pages-deploy-action@v4
  with:
    folder: deploy_root
    clean: true
    target-folder: ${{ steps.calc.outputs.target_folder }}
    clean-exclude: ${{ env.clean_exclude }}

We ensure a clean deployment by computing the clean-exclude list based on the versions file:

CLEAN_EXCLUDE=".nojekyll"
NEW_VERSIONS_FILE="version_root/$(basename "${{ inputs.versions_file }}")"

echo "Checking versions file: $NEW_VERSIONS_FILE"

if [[ "${{ steps.calc.outputs.target_folder }}" == "/" ]]; then
  if [[ -f "$NEW_VERSIONS_FILE" ]]; then
    echo "✅ Versions file found!"
    VERSIONS=$(grep -v '^/$' "$NEW_VERSIONS_FILE")
    CLEAN_EXCLUDE=".nojekyll"$'
'"$VERSIONS"
  else
    echo "❌ ERROR: Versions file not found!"
    exit 1
  fi
fi

echo -e "Final CLEAN_EXCLUDE:
$CLEAN_EXCLUDE"
{
  echo "clean_exclude<<EOF"
  echo "$CLEAN_EXCLUDE"
  echo "EOF"
} >> "$GITHUB_ENV"

2. Deploying to an External Repository using JamesIves/github-pages-deploy-action

The JamesIves/github-pages-deploy-action GitHub Action allows us to deploy our project to an external repository by specifying the repository-name parameter and providing the necessary authentication credentials.

Configuration for External Repository Deployment

To deploy to an external repository, we need configure the action with:

  • The repository-name parameter.
  • Authentication via either a Personal Access Token (PAT) or an SSH Private Key.

Using a Personal Access Token (PAT)

A Personal Access Token is required to push changes to a different repository. We need to create a PAT with the necessary permissions (typically the repo scope) and store it as a secret in our repository settings.

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
..........................................
      - name: Deploy to External Repository
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          token: ${{ secrets.DEPLOY_TOKEN }}
          repository-name: score/score-preview
          branch: gh-pages
          folder: build
          clean: true

Note: DEPLOY_TOKEN should be stored as a GitHub secret in our repository.

Using an SSH Private Key

Alternatively, SSH keys can be used for authentication. Add the private key as a GitHub secret and add the corresponding public key to the target repository’s Deploy Keys with write access.

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
................
      - name: Deploy to External Repository via SSH
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          ssh-key: ${{ secrets.SSH_DEPLOY_KEY }}
          repository-name: score /score-preview
          branch: gh-pages
          folder: build
          clean: true

Conclusion

When allowing PRs from forks to push to GitHub Pages, using pull_request_target is necessary to access repository secrets. However, due to the security risks, additional safeguards should be implemented:

  1. Require maintainer approval via an environment.
  2. Check user permissions before allowing deployment.
  3. Adapt workflows to correctly handle pull_request_target by explicitly checking out the correct repository.
  4. Consider (optional) deploying to a separate repository to avoid granting unnecessary write access.

By following these best practices, we can safely enable contributors to publish changes to GitHub Pages while maintaining security within our score repository.

@AlexanderLanin
Copy link
Member Author

Thanks for that! It is a nice read!
It really looks like this article is worth more than the actual implementation!

How about converting it to .github/workflows/decision-records/pr-preview.md including the alternatives you have evaluated and why they are no good.

@everyone: if you know any github experts, please ask them for a review!

@AlexanderLanin
Copy link
Member Author

@dcalavrezo-qorix would it make sense to implement 1-3 and then revisit 4? Or is it too much overhead?

@dcalavrezo-qorix
Copy link
Contributor

@AlexanderLanin it makes sense. The overhead would be low.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs-as-code infrastructure General Score infrastructure topics
Projects
Status: In Progress
Development

No branches or pull requests

2 participants