Create and synchronize a PR in a dependent repository
Go to file
2024-03-20 15:47:35 +00:00
.forgejo/workflows also run the tests on v* branches 2024-02-19 13:59:22 +01:00
tests remove branches when closing a cascade PR from a fork 2024-01-09 19:48:08 +01:00
.editorconfig .editorconfig 2023-10-14 20:46:18 +02:00
.gitignore login both repos 2023-10-12 17:08:13 +02:00
action.yml do not call actions/checkout 2024-02-19 12:36:39 +01:00 allow running on a ref instead of a PR 2024-01-03 16:12:23 +01:00 there may be more than one cascading PR from the same origin 2024-03-20 16:23:28 +01:00 install dependencies 2023-10-13 15:01:47 +02:00
LICENSE LICENSE 2023-10-13 19:47:35 +02:00 close-merge is close and applies to destination-ref as well 2024-01-09 19:47:48 +01:00

Create and synchronize a PR in a dependent repository


If repository A depends on repository B, cascadinging-pr can be used by a workflow in repository B to trigger the CI on repository A and verify it passes when using a modified version of repository B. This modified version could be a pull request, a branch or a reference.

In the simplest case cascading-pr runs a workflow in destination-repo that uses origin-ref and blocks until it completes.

As an example, when a tag is set in Forgejo and builds a new release, it is concluded by a call to cascading-pr that runs end-to-end tests on the newly built release to verify it works as expected.

When used in a workflow triggered by a PR event in origin-repo, cascading-pr will create, update and close a matching PR in the destination-repo. When the PR is updated, cascading-pr will update the matching PR. It waits for the workflow triggered by these updates in destination-repo to complete. If fails, cascading-pr, also fails.

As an example, when a PR is created in forgejo/runner, a matching PR is created in actions/setup-forgejo with the proposed change and cascading-pr waits until the CI in actions/setup-forgejo is successful.

The update script is expected to be found in origin-repo and is given the following arguments:

  • A directory path in which the destination-branch of destination-repo (or a fork) is checked-out.
  • The path to a JSON file describing the pull request in destination-repo.
  • A directory path in which the head of origin-repo is checked-out at:
    • if origin-pr is specified, the head branch of origin-pr
    • otherwise origin-ref
  • Information about the origin-repo
    • if origin-pr is specified, the path to a JSON file desccribing the pull request in origin-repo
    • otherwise origin-ref

If changes are found in the destination repository directory after the update script runs, they will be pushed as a new commit in the PR in the destination-repo.

origin-token is used when accessing origin-repo and needs the read:user, read:repository and write:issue scopes.

destination-token is used to push the branch that contains an update to destination-repo (or destination-fork-repo) and open a pull request. It needs the read:user, write:repository and write:issue scopes.

It is recommended that a dedicated user is used to create destination-token and that destination-fork-repo is always used unless the users who are able to create pull requests are trusted.

When the PR in the destination-repo is from a forked repository, the update script is run from the default branch of destination-repo instead of the head of the PR which is a branch in destination-fork-repo. The PR author must not be trusted and it is imperative that the update script never runs anything found in the head branch of the PR.

If the fork of the destination repository is specified and it does not exist, it is created.


parameter description required default
origin-url URL of the Forgejo instance where the PR that triggers the action is located (e.g. true
origin-repo the repository in which the PR was created true
origin-token a token with write permission on origin-repo true
origin-pr number of the PR in {orign-repo}, mutually exclusive with {origin-ref} false
origin-ref reference in {orign-repo}, mutually exclusive with {origin-pr} false
destination-url URL of the Forgejo instance where the cascading PR is created or updated (e.g. true
destination-repo the repository in which the cascading PR is created or updated true
destination-fork-repo the fork of {destination-repo} in which the {destination-branch} will be created or updated false
destination-branch the base branch of the destination repository for the cascading PR true
destination-token a token with write permission on destination-repo true
update path to the script to update the content of the cascading PR true
prefix prefix of the branch from which the cascading PR is created on {destination-repo} or {destination-fork-repo} (default to {origin-repo}) false
close if true the cascading PR will be closed and the branch deleted when the PR is merged false false
verbose if true print verbose information false false
debug if true print debug information false false

Forgejo dependencies

The Forgejo repositories that depend on each other are linked with workflows using cascading-pr as follows.

flowchart TD
    lxc-helper(lxc-helper) --> act(act)
    act --> runner(Forgejo runner)
    runner --> setup-forgejo(setup-forgejo)
    setup-forgejo --> e2e(end-to-end)
    forgejo-curl( --> setup-forgejo
    forgejo(forgejo) --> e2e

    click lxc-helper ""
    click act ""
    click runner ""
    click setup-forgejo ""
    click e2e ""
    click forgejo-curl ""
    click forgejo ""

Example workflow

      - opened
      - synchronize
      - closed

    runs-on: docker
      - uses: actions/checkout@v4
      - uses: actions/cascading-pr@v1
          origin-repo: forgejo/lxc-helpers
          origin-token: ${{ secrets.ORIGIN_TOKEN }}
          origin-pr: ${{ github.event.pull_request.number }}
          destination-repo: forgejo/act
          destination-branch: main
          destination-token: ${{ secrets.DESTINATION_TOKEN }}
          update: ./upgrade-lxc-helpers

Pull requests from forked repositories

When cascading-pr runs as a consequence of pull request from a forked repository, the workflow must be triggered by a pull_request_target event otherwise it will not have access to secrets.

Prevent privilege escalation

When cascading-pr runs as a consequence of a pull request from a repository forked from orgin-repo, it should create a pull request from a forked repository of destination-repo by specifying the destination-fork-repo.

If the destination-fork-repo repository does not exist, it will be created as a fork of the destination-repo repository, using destination-token.


The test environment consists of the following (all users password is admin1234)

  • A forgejo instance with a runner
  • An unprivileged user user1
  • The repository user1/originrepo
    • contains a pull_request workflow using cascading-pr that targets user2/destinationrepo
    • contains a script that will modify user2/destinationrepo
    • a branch1 at the same commit as main
  • The repository user1/cascading-pr with the action under test
  • An unprivileged user user2
  • The repository user2/destinationrepo
git clone
export PATH=$(pwd)/setup-forgejo:$PATH
git clone
cd cascading-pr
export DIR=/tmp/forgejo-for-cascading-pr logout teardown teardown setup root admin1234
FORGEJO_RUNNER_CONFIG=$(pwd)/tests/runner-config.yaml setup
url=$(cat $DIR/forgejo-url)
firefox $url

The test for a successful run of the cascading-pr action consists of:

  • creating a PR from branch1 to main
  • wait for the commit status until it is successful

testing an update on the action

  • run tests/ --debug once so all is in place
  • commit changes to the files that are in the cascading-pr action (action.yml, etc.)
  • push the modified action to user1/cascading-pr
  • visit $url/user1/originrepo/actions/runs/1 and click re-run

interactive debugging

Following the steps below recreate the same environment as the integration workflow locally. It is helpful for forensic analysis when something does not run as expected and the error displayed are unclear.

To help with the development loop all steps are idempotent and running tests/ --debug multiple times must succeed.

Individual steps can be run independendely by using the name of the function. For instance:

  • tests/ --debug create_pull_request will only call the create_pull_request function found in tests/ to (re)create the pull request in user1/originrepo.
  • ./ --debug --origin-url ... upsert_branch will only call the upsert_branch function found in


If --debug is used a full debug log is displayed, very complete and very verbose. Otherwise it is stashed in a temporary file and only displayed if an error happens.

If all goes well, the runner logs are not displayed even with --debug. They can be looked at in the web UI.


The tests/ script stores all its files in /tmp/cascading-pr-test. The temporary directories created by are disposed of when the script ends.

snippets for copy/pasting

tests/ --debug
tests/ --debug no_change_no_cascade_pr
./ --debug --origin-url "$url" --origin-repo "user1/originrepo" --origin-token "$(cat /tmp/cascading-pr-test/user1/repo-token)" --origin-pr 1 --destination-url "$url" --destination-repo "user2/destinationrepo" --destination-token "$(cat /tmp/cascading-pr-test/user2/repo-token)" --destination-branch "main" --update "upgraded" run

Update the README

With action-docs --update-readme