diff --git a/README.md b/README.md index f31c5ff..e582a89 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/action.yml b/action.yml index 051682a..8d64419 100644 --- a/action.yml +++ b/action.yml @@ -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 }}" \ diff --git a/cascading-pr-lib.sh b/cascading-pr-lib.sh index bdbf86f..0991a0a 100644 --- a/cascading-pr-lib.sh +++ b/cascading-pr-lib.sh @@ -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 +} diff --git a/cascading-pr.sh b/cascading-pr.sh index 1887f57..4b07548 100755 --- a/cascading-pr.sh +++ b/cascading-pr.sh @@ -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 diff --git a/tests/destination-fail/.forgejo/workflows/test.yml b/tests/destination-fail/.forgejo/workflows/test.yml new file mode 100644 index 0000000..9144340 --- /dev/null +++ b/tests/destination-fail/.forgejo/workflows/test.yml @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: MIT +name: test +on: [pull_request_target] +jobs: + test: + runs-on: docker + steps: + - run: false diff --git a/tests/origin-branch-fail/.forgejo/workflows/test.yml b/tests/origin-branch-fail/.forgejo/workflows/test.yml new file mode 100644 index 0000000..e405038 --- /dev/null +++ b/tests/origin-branch-fail/.forgejo/workflows/test.yml @@ -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 diff --git a/tests/origin-branch-fail/README b/tests/origin-branch-fail/README new file mode 100644 index 0000000..d2b2178 --- /dev/null +++ b/tests/origin-branch-fail/README @@ -0,0 +1 @@ +originrepo diff --git a/tests/origin-branch-fail/upgraded b/tests/origin-branch-fail/upgraded new file mode 100755 index 0000000..4c2635f --- /dev/null +++ b/tests/origin-branch-fail/upgraded @@ -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 diff --git a/tests/origin-branch/.forgejo/workflows/test.yml b/tests/origin-branch/.forgejo/workflows/test.yml new file mode 100644 index 0000000..2d1ce52 --- /dev/null +++ b/tests/origin-branch/.forgejo/workflows/test.yml @@ -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 diff --git a/tests/origin-branch/README b/tests/origin-branch/README new file mode 100644 index 0000000..d2b2178 --- /dev/null +++ b/tests/origin-branch/README @@ -0,0 +1 @@ +originrepo diff --git a/tests/origin-branch/upgraded b/tests/origin-branch/upgraded new file mode 100755 index 0000000..4c2635f --- /dev/null +++ b/tests/origin-branch/upgraded @@ -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 diff --git a/tests/run.sh b/tests/run.sh index 5d4a43d..8b116e0 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -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 }