1
0
Fork 0
mirror of https://code.forgejo.org/actions/cascading-pr synced 2025-03-14 22:36:58 +01:00

allow running on a ref instead of a PR

When running from a PR, the ref is the head of the PR in the origin
repository. The PR is used to:

* display comments about the location of the destination PR
* asynchronously close / merge the destination PR when something
  happens in the origin PR

When only the ref is available, the destination PR must be closed and
the corresponding branch destroyed immediately after it concludes
because there is no convenient way to know what or when something
else will happen.
This commit is contained in:
Earl Warren 2024-01-02 18:39:58 +01:00
parent 0e9a4e846b
commit 0c3c8b591b
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
12 changed files with 326 additions and 85 deletions

View file

@ -4,58 +4,69 @@ Create and synchronize a PR in a dependent repository
## Description
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 it will upgrade with the proposed change
from repository B.
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](https://code.forgejo.org/forgejo/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
another repository (`destination-repo`). When the PR is updated,
`cascading-pr` subsequently will update the matching PR. The
worfklows in `origin-repo` will wait for the workflow in
`destination-repo` to complete. If the workflow in
`destination-repo` fails, the workflow in `origin-repo` will also
fail.
`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`](https://code.forgejo.org/forgejo/runner/), a
matching PR is created in
[`actions/setup-forgejo`](https://code.forgejo.org/actions/setup-forgejo/)
with the proposed change. `cascading-pr` will wait until the CI in
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 the origin repository
running the PR. It is given four arguments:
The `update` script is expected to be found in `origin-repo` and is
given the following arguments:
* A directory in which the destination repository (or a fork) is checked-out
on the base branch
* A file with the JSON describing the pull request in the
destination repository
* A directory in which the origin repository is checked-out
on the head branch
* A file with the JSON describing the pull request in the
origin repository
* 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.
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` and to open a pull request. It needs
the `read:user`, `write:repository` and `write:issue` scopes.
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 is from a forked repository, the `update` script is run
from the default branch of the base repository instead of the head
branch of the fork. The pull request author must not be trusted
and it is imperative that the `update` script never runs anything
found in the head branch of the pull request.
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.
@ -68,7 +79,8 @@ not exist, it is created.
| origin-url | URL of the Forgejo instance where the PR that triggers the action is located (e.g. https://code.forgejo.org) | `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} | `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. https://code.forgejo.org) | `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` | |
@ -168,12 +180,13 @@ git clone https://code.forgejo.org/actions/setup-forgejo
export PATH=$(pwd)/setup-forgejo:$PATH
git clone https://code.forgejo.org/actions/cascading-pr
cd cascading-pr
export DIR=/tmp/forgejo-for-cascading-pr
forgejo-curl.sh logout
forgejo-runner.sh teardown
forgejo.sh teardown
forgejo.sh setup root admin1234 codeberg.org/forgejo/forgejo 1.21
forgejo-binary.sh teardown
forgejo-binary.sh setup root admin1234 https://codeberg.org/forgejo/forgejo/releases/download/v1.21.3-0/forgejo-1.21.3-0-linux-amd64
FORGEJO_RUNNER_CONFIG=$(pwd)/tests/runner-config.yaml forgejo-runner.sh setup
url=http://$(cat forgejo-ip):3000
url=$(cat $DIR/forgejo-url)
firefox $url
```

View file

@ -3,58 +3,69 @@ name: 'Cascading PR'
author: 'Forgejo authors'
description: |
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 it will upgrade with the proposed change
from repository B.
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](https://code.forgejo.org/forgejo/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
another repository (`destination-repo`). When the PR is updated,
`cascading-pr` subsequently will update the matching PR. The
worfklows in `origin-repo` will wait for the workflow in
`destination-repo` to complete. If the workflow in
`destination-repo` fails, the workflow in `origin-repo` will also
fail.
`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`](https://code.forgejo.org/forgejo/runner/), a
matching PR is created in
[`actions/setup-forgejo`](https://code.forgejo.org/actions/setup-forgejo/)
with the proposed change. `cascading-pr` will wait until the CI in
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 the origin repository
running the PR. It is given four arguments:
The `update` script is expected to be found in `origin-repo` and is
given the following arguments:
* A directory in which the destination repository (or a fork) is checked-out
on the base branch
* A file with the JSON describing the pull request in the
destination repository
* A directory in which the origin repository is checked-out
on the head branch
* A file with the JSON describing the pull request in the
origin repository
* 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.
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` and to open a pull request. It needs
the `read:user`, `write:repository` and `write:issue` scopes.
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 is from a forked repository, the `update` script is run
from the default branch of the base repository instead of the head
branch of the fork. The pull request author must not be trusted
and it is imperative that the `update` script never runs anything
found in the head branch of the pull request.
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.
@ -70,8 +81,9 @@ inputs:
description: 'a token with write permission on origin-repo'
required: true
origin-pr:
description: 'number of the PR in {orign-repo}'
required: true
description: 'number of the PR in {orign-repo}, mutually exclusive with {origin-ref}'
origin-ref:
description: 'reference in {orign-repo}, mutually exclusive with {origin-pr}'
destination-url:
description: 'URL of the Forgejo instance where the cascading PR is created or updated (e.g. https://code.forgejo.org)'
required: true
@ -125,6 +137,7 @@ runs:
--origin-repo "${{ inputs.origin-repo }}" \
--origin-token "@$origin_token" \
--origin-pr "${{ inputs.origin-pr }}" \
--origin-ref "${{ inputs.origin-ref }}" \
--destination-url "${{ inputs.destination-url }}" \
--destination-repo "${{ inputs.destination-repo }}" \
--destination-fork-repo "${{ inputs.destination-fork-repo }}" \

View file

@ -197,3 +197,47 @@ function wait_status() {
return 1
fi
}
function sanity_check_pr_or_ref() {
local pr="$1" ref="$2"
if test "$pr" -a "$ref" ; then
log_error "--origin-pr $pr and --origin-ref $ref are mutually exclusive"
return 1
fi
if test -z "$pr" -a -z "$ref" ; then
log_error "one of --origin-pr or --origin-ref must be set"
return 2
fi
}
function set_origin_head() {
local pr="${options[origin_pr]}"
local ref="${options[origin_ref]}"
sanity_check_pr_or_ref "$pr" "$ref"
if test "$pr"; then
options[origin_head]=refs/pull/$pr/head
origin_sanity_check
else
options[origin_head]=$ref
fi
}
function origin_has_pr() {
test "${options[origin_pr]}"
}
function set_destination_head() {
local pr="${options[origin_pr]}"
local ref="${options[origin_ref]}"
sanity_check_pr_or_ref "$pr" "$ref"
if $(origin_has_pr); then
options[destination_head]=${options[prefix]}-$pr
else
options[destination_head]=${options[prefix]}-$ref
fi
}

View file

@ -187,7 +187,7 @@ function git_checkout() {
(
cd $TMPDIR/$direction
if [[ "$ref" =~ ^refs/ ]] ; then
git fetch ${remote} +$ref:$ref
git fetch --update-head-ok ${remote} +$ref:$ref
else
ref=${remote}/$ref
fi
@ -305,7 +305,7 @@ function update() {
local update=${options[update]}
if ! [[ "$update" =~ ^/ ]] ; then
local d
if $(pr_from_fork origin); then
if $(origin_has_pr) && $(pr_from_fork origin); then
local default_branch=$(default_branch origin)
log_info "PR is from a forked repository, using the default branch $default_branch to obtain the update script"
d=$TMPDIR/update
@ -316,7 +316,13 @@ function update() {
update=$d/$update
fi
cd $TMPDIR
$update $TMPDIR/destination $TMPDIR/destination-pr.json $TMPDIR/origin $TMPDIR/origin-pr.json
local origin_info
if $(origin_has_pr); then
origin_info=$TMPDIR/origin-pr.json
else
origin_info="${options[origin_ref]}"
fi
$update $TMPDIR/destination $TMPDIR/destination-pr.json $TMPDIR/origin $origin_info
)
local remote_head=origin
if ${options[destination_is_fork]} ; then
@ -360,8 +366,7 @@ function finalize_options() {
options[origin_scheme]=$(scheme ${options[origin_url]})
options[origin_host_port]=$(host_port ${options[origin_url]})
set_git_url origin origin_clone ${options[origin_repo]}
options[origin_head]=refs/pull/${options[origin_pr]}/head
origin_sanity_check
set_origin_head
options[destination_api]=${options[destination_url]}/api/v1/repos/${options[destination_repo]}
options[destination_scheme]=$(scheme ${options[destination_url]})
@ -369,7 +374,7 @@ function finalize_options() {
set_git_url destination destination_clone ${options[destination_repo]}
options[destination_base]=${options[destination_branch]}
: ${options[prefix]:=${options[origin_repo]}}
options[destination_head]=${options[prefix]}-${options[origin_pr]}
set_destination_head
if test "${options[destination_fork_repo]}"; then
fork_sanity_check
@ -384,11 +389,39 @@ function finalize_options() {
}
function run() {
local state=$(pr_state origin)
repo_login origin
repo_login destination
if $(origin_has_pr); then
run_origin_pr
else
run_origin_ref
fi
}
function run_origin_ref() {
log_info "update or create the cascade branch and PR"
checkout
update
local sha=$(sha_pushed destination)
if test "$sha" ; then
upsert_destination_pr
local status
if wait_destination_ci "$sha" ; then
log_info "cascade PR status successful"
status=0
else
log_info "cascade PR status failed"
status=1
fi
close_pr
return $status
fi
}
function run_origin_pr() {
local state=$(pr_state origin)
case "$state" in
open)
log_info "PR is open, update or create the cascade branch and PR"
@ -455,6 +488,11 @@ function main() {
options[origin_pr]=$1
shift
;;
--origin-ref)
shift
options[origin_ref]=$1
shift
;;
--destination-url)
shift
options[destination_url]=$1

View file

@ -0,0 +1,8 @@
# SPDX-License-Identifier: MIT
name: test
on: [pull_request_target]
jobs:
test:
runs-on: docker
steps:
- run: false

View file

@ -0,0 +1,20 @@
# SPDX-License-Identifier: MIT
name: test
on: [push]
jobs:
test:
runs-on: docker
steps:
- uses: SELF@vTest
with:
origin-url: ${{ env.GITHUB_SERVER_URL }}
origin-repo: user1/origin-branch-fail
origin-token: ${{ secrets.ORIGIN_TOKEN }}
origin-ref: refs/heads/main
destination-url: ${{ env.GITHUB_SERVER_URL }}
destination-repo: user2/destination-fail
destination-branch: main
destination-token: ${{ secrets.DESTINATION_TOKEN }}
update: ./upgraded
debug: true

View file

@ -0,0 +1 @@
originrepo

View file

@ -0,0 +1,14 @@
#!/bin/bash
set -ex
destination_checkout="$1"
destination_pr_json="$2"
origin_checkout="$3"
origin_ref="$4"
test -d $destination_checkout
test -d $origin_checkout
test "$origin_ref"
date +%s > $destination_checkout/last

View file

@ -0,0 +1,20 @@
# SPDX-License-Identifier: MIT
name: test
on: [push]
jobs:
test:
runs-on: docker
steps:
- uses: SELF@vTest
with:
origin-url: ${{ env.GITHUB_SERVER_URL }}
origin-repo: user1/origin-branch
origin-token: ${{ secrets.ORIGIN_TOKEN }}
origin-ref: refs/heads/main
destination-url: ${{ env.GITHUB_SERVER_URL }}
destination-repo: user2/destinationrepo
destination-branch: main
destination-token: ${{ secrets.DESTINATION_TOKEN }}
update: ./upgraded
debug: true

View file

@ -0,0 +1 @@
originrepo

14
tests/origin-branch/upgraded Executable file
View file

@ -0,0 +1,14 @@
#!/bin/bash
set -ex
destination_checkout="$1"
destination_pr_json="$2"
origin_checkout="$3"
origin_ref="$4"
test -d $destination_checkout
test -d $origin_checkout
test "$origin_ref"
date +%s > $destination_checkout/last

View file

@ -79,17 +79,35 @@ function merge_pull_request() {
}
function has_cascade_pull_request() {
log_verbose "verify a cascade pull request exists"
test "$(cascade_pull_request_count)" -gt 0
local repo="${1:-destinationrepo}"
log_verbose "verify an open cascade pull request exists"
test "$(cascade_open_pull_request_count $repo)" -gt 0
}
function has_no_cascade_pull_request() {
log_verbose "verify there is no cascade pull request"
test "$(cascade_pull_request_count)" = 0
local repo="${1:-destinationrepo}"
log_verbose "verify there is no open cascade pull request"
test "$(cascade_open_pull_request_count $repo)" = 0
}
function cascade_open_pull_request_count() {
local repo="$1"
cascade_pull_request $repo | jq '[ .[] | select(.state == "open") ] | length'
}
function cascade_pull_request_count() {
forgejo-curl.sh api_json ${options[url]}/api/v1/repos/user2/destinationrepo/pulls | jq '[ .[] | select(.state == "open") ] | length'
local repo="$1"
cascade_pull_request $repo | jq '. | length'
}
function cascade_pull_request() {
local repo="$1"
forgejo-curl.sh api_json ${options[url]}/api/v1/repos/user2/${repo}/pulls
}
function create_branch1() {
@ -150,6 +168,13 @@ function create_pull_request_case1() {
create_pull_request $baseowner $headowner $repo
}
function unit_finalize_options() {
sanity_check_pr_or_ref A B || test $? = 1
sanity_check_pr_or_ref '' '' || test $? = 2
sanity_check_pr_or_ref A ''
sanity_check_pr_or_ref '' B
}
function unit_retry_fail() {
local file=$1
local value=$(cat $file)
@ -246,6 +271,34 @@ function no_change_no_cascade_pr() {
has_no_cascade_pull_request
}
function branch_and_success() {
local origin_repo=origin-branch
local destination_repo=destinationrepo
fixture ${origin_repo} ${destination_repo}
user_secret user1 DESTINATION_TOKEN $(user_token user2 DESTINATION_TOKEN)
test $(cascade_pull_request_count ${destination_repo}) = 0
create_branch1 user1 ${origin_repo}
wait_success ${options[url]}/api/v1/repos/user1/${origin_repo} $(cat $TMPDIR/user1-${origin_repo}.sha)
test $(cascade_pull_request_count ${destination_repo}) = 1
has_no_cascade_pull_request ${destination_repo}
}
function branch_and_fail() {
local origin_repo=origin-branch-fail
local destination_repo=destination-fail
fixture ${origin_repo} ${destination_repo}
user_secret user1 DESTINATION_TOKEN $(user_token user2 DESTINATION_TOKEN)
test $(cascade_pull_request_count ${destination_repo}) = 0
create_branch1 user1 ${origin_repo}
wait_failure ${options[url]}/api/v1/repos/user1/${origin_repo} $(cat $TMPDIR/user1-${origin_repo}.sha)
test $(cascade_pull_request_count ${destination_repo}) = 1
has_no_cascade_pull_request ${destination_repo}
}
function create_and_close() {
fixture originrepo destinationrepo
user_secret user1 DESTINATION_TOKEN $(user_token user2 DESTINATION_TOKEN)
@ -367,6 +420,7 @@ function run() {
}
function integration() {
run branch_and_success
run no_change_no_cascade_pr
run create_in_destination_fork_and_close
run create_and_close
@ -379,6 +433,7 @@ function integration() {
}
function unit() {
unit_finalize_options
unit_retry
}
@ -388,12 +443,12 @@ function run_tests() {
}
function finalize_options() {
if test -f forgejo-ip; then
: ${options[host_port]:=$(cat forgejo-ip):3000}
if test -f $DIR/forgejo-ip; then
: ${options[host_port]:=$(cat $DIR/forgejo-ip):3000}
fi
options[url]=http://${options[host_port]}
if test -f forgejo-token; then
: ${options[token]:=$(cat forgejo-token)}
if test -f $DIR/forgejo-token; then
: ${options[token]:=$(cat $DIR/forgejo-token)}
fi
options[password]=admin1234
}