From 5f8f2624726a60cdac1d19310342c10772679e1a Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Wed, 11 Oct 2023 18:05:11 +0200 Subject: [PATCH] split out the lib --- action.yml | 51 +++++-- cascading-pr-lib.sh | 52 +++++++ cascading-pr.sh | 66 +++++++++ forgejo-curl.sh | 338 ++++++++++++++++++++++++++++++++++++++++++++ tests/run.sh | 59 ++------ 5 files changed, 508 insertions(+), 58 deletions(-) create mode 100644 cascading-pr-lib.sh create mode 100644 cascading-pr.sh create mode 100644 forgejo-curl.sh diff --git a/action.yml b/action.yml index df47e7b..a39b271 100644 --- a/action.yml +++ b/action.yml @@ -5,20 +5,55 @@ description: | Create and update a PR in another repository inputs: - forgejo: - description: 'URL of the Forgejo instance where the PR is created (e.g. https://code.forgejo.org)' + origin-url: + description: 'URL of the Forgejo instance where the PR that triggers the action is located (e.g. https://code.forgejo.org)' required: true - repo: - description: 'the repository into which the PR is created' + origin-repo: + description: 'the repository in which the PR was created' required: true - token: - description: 'a token with write permission on repo' + origin-token: + description: 'a token with write permission on origin-repo' required: true + destination-url: + description: 'URL of the Forgejo instance where the cascading PR is created or updated (e.g. https://code.forgejo.org)' + required: true + destination-repo: + description: 'the repository in which the cascading PR is created or updated' + required: true + destination-branch: + description: 'the base branch of the destination repository for the cascading PR' + required: true + destination-token: + description: 'a token with write permission on destination-repo' + required: true + update: + description: 'path to the script to update the content of the cascading PR' + required: true + verbose: + description: 'if true print verbose information' + default: false + debug: + description: 'if true print debug information' + default: false runs: using: "composite" steps: - uses: actions/checkout@v4 - run: | - echo - shell: bash + if "${{ inputs.verbose }}"; then + verbosity="$verbosity --verbose" + fi + if "${{ inputs.debug }}"; then + verbosity="$verbosity --debug" + fi + + cascading-pr.sh $verbosity \ + --origin-url "${{ inputs.origin-url }}" \ + --origin-repo "${{ inputs.origin-repo }}" \ + --origin-token "${{ inputs.origin-token }}" \ + --destination-url "${{ inputs.destination-url }}" \ + --destination-repo "${{ inputs.destination-repo }}" \ + --destination-token "${{ inputs.destination-token }}" \ + --destination-branch "${{ inputs.destination-branch }}" \ + --update "${{ inputs.update }}" diff --git a/cascading-pr-lib.sh b/cascading-pr-lib.sh new file mode 100644 index 0000000..ff33f9e --- /dev/null +++ b/cascading-pr-lib.sh @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: MIT + +declare -A options + +VERBOSE=false +DEBUG=false +: ${EXIT_ON_ERROR:=true} + +function debug() { + DEBUG=true + set -x + PS4='${BASH_SOURCE[0]}:$LINENO: ${FUNCNAME[0]}: ' +} + +function verbose() { + VERBOSE=true +} + +function log() { + echo "$@" >&2 +} + +function log_error() { + log "$@" +} + +function log_verbose() { + if $VERBOSE ; then + log "$@" + fi +} + +function log_info() { + log "$@" +} + +function fatal_error() { + log_error "$@" + if $EXIT_ON_ERROR ; then + exit 1 + else + return 1 + fi +} + +function host_port() { + local url="$1" + + local host_port="${url##http*://}" + echo ${host_port%%/} +} + diff --git a/cascading-pr.sh b/cascading-pr.sh new file mode 100644 index 0000000..2af8f8a --- /dev/null +++ b/cascading-pr.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# SPDX-License-Identifier: MIT + +SELF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source $SELF_DIR/cascading-pr-lib.sh + +function run() { + options[origin_host_port]=$(host_port ${options[origin_url]}) + options[destination_host_port]=$(host_port ${options[destination_url]}) + + # login destination + # open a PR on destination + # checkout the head of the PR + # update the PR + # force-push the head + # wait on the status of the tip of the head +} + +function main() { + while true; do + case "$1" in + --verbose) + shift + verbose + ;; + --debug) + shift + debug + ;; + --origin-url) + shift + options[origin_url]=$1 + ;; + --origin-repo) + shift + options[origin_repo]=$1 + ;; + --origin-token) + shift + options[origin_token]=$1 + ;; + --destination-url) + shift + options[destination_url]=$1 + ;; + --destination-repo) + shift + options[destination_repo]=$1 + ;; + --destination-token) + shift + options[destination_token]=$1 + ;; + --update) + shift + options[update]=$1 + ;; + *) + "${1:-run}" + return 0 + ;; + esac + done +} + +${MAIN:-main} "${@}" diff --git a/forgejo-curl.sh b/forgejo-curl.sh new file mode 100644 index 0000000..3397683 --- /dev/null +++ b/forgejo-curl.sh @@ -0,0 +1,338 @@ +#!/bin/bash +# SPDX-License-Identifier: MIT + +VERSION=1.0.0 +SELF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +VERBOSE=false +DEBUG=false +: ${EXIT_ON_ERROR:=true} +: ${TOKEN_NAME:=forgejo-curl} +: ${DOT:=$HOME/.forgejo-curl} + +function debug() { + DEBUG=true + set -x + PS4='${BASH_SOURCE[0]}:$LINENO: ${FUNCNAME[0]}: ' +} + +function verbose() { + VERBOSE=true +} + +function log() { + echo "$@" >&2 +} + +function log_error() { + log "$@" +} + +function log_verbose() { + if $VERBOSE ; then + log "$@" + fi +} + +function log_info() { + log "$@" +} + +function fatal_error() { + log_error "$@" + if $EXIT_ON_ERROR ; then + exit 1 + else + return 1 + fi +} + +function dot_ensure() { + mkdir -p $DOT +} + +HEADER_JSON='-H Content-Type:application/json' +HEADER_TOKEN="-H @$DOT/header-token" +HEADER_CSRF="-H @$DOT/header-csrf" + +function api() { + client $HEADER_TOKEN "$@" +} + +function api_json() { + api $HEADER_JSON "$@" +} + +function login_api() { + local user="$1" password="$2" token="$3" scopes="${4:-[\"all\"]}" url="$5" + + dot_ensure + if test -s $DOT/token ; then + log_info "already logged in, ignored" + return + fi + + if test -z "$token" ; then + log_verbose curl -sS -X DELETE --user "${user}:${password}" "${url}/api/v1/users/$user/tokens/${TOKEN_NAME}" -o /dev/null -w "%{http_code}" + local basic="${user:-unknown}:${password:-unknown}" + local status=$(curl -sS -X DELETE --user "${basic}" "${url}/api/v1/users/$user/tokens/${TOKEN_NAME}" -o /dev/null -w "%{http_code}") + if test "${status}" != 404 -a "${status}" != 204 ; then + fatal_error permission denied, the user or password are probably incorrect, try again with --verbose + return 1 + fi + token=$(client $HEADER_JSON --user "${basic}" --data-raw '{"name":"'${TOKEN_NAME}'","scopes":'${scopes}'}' "${url}/api/v1/users/${user}/tokens" | jq --raw-output .sha1) + fi + if [[ "$token" =~ ^@ ]] ; then + cp "${token##@}" $DOT/token + else + echo "$token" > $DOT/token + fi + ( echo -n "Authorization: token " ; cat $DOT/token ) > $DOT/header-token + # + # Verify it works + # + local status=$(api -w "%{http_code}" -o /dev/null "${url}/api/v1/user") + if test "${status}" != 200 ; then + fatal_error "${url}/api/v1/user returns status code '${status}', the token is invalid, $0 logout and login again" + return 1 + fi +} + +function client() { + log_verbose curl --cookie $DOT/cookies -f -sS "$@" + if ! curl --cookie $DOT/cookies -f -sS "$@" ; then + fatal_error + fi +} + +function web() { + client $HEADER_CSRF "$@" +} + +function client_update_cookies() { + log_verbose curl --cookie-jar $DOT/cookies --cookie $DOT/cookies -w "%{http_code}" -f -sS "$@" + local status=$(curl --cookie-jar $DOT/cookies --cookie $DOT/cookies -w "%{http_code}" -f -sS "$@") + if ! test "${status}" = 200 -o "${status}" = 303 ; then + fatal_error + fi +} + +function login_client() { + local user="$1" password="$2" url="$3" + + if test -z "$password" ; then + log_verbose "no password, web will not be authenticated" + return + fi + + dot_ensure + # + # Get the CSRF required for login + # + client_update_cookies -o /dev/null "${url}/user/login" + # + # The login stores a cookie + # + client_update_cookies -X POST --data "user_name=${user}" --data "password=${password}" "${url}/user/login" -o $DOT/login.html + # + # Get the CSRF for reuse by other requests + # + client_update_cookies -o /dev/null "${url}/user/login" + local csrf=$(sed -n -e '/csrf/s/.*csrf\t//p' $DOT/cookies) + echo "X-Csrf-Token: $csrf" > $DOT/header-csrf + # + # Verify it works + # + local status=$(web -o /dev/null -w "%{http_code}" "${url}/user/settings") + if test "${status}" != 200 ; then + grep -C 1 flash-error $DOT/login.html + if ${DEBUG} ; then + cat $DOT/login.html + fi + fatal_error login failed, the user or password are probably incorrect, try again with --verbose + fi +} + +function login() { + local user="$1" password="$2" token="$3" scope="$4" url="$5" + login_client "${user}" "${password}" "${url}" + login_api "${user}" "${password}" "${token}" "${scope}" "${url}" +} + +function logout() { + rm -f $DOT/* + if test -d $DOT ; then + rmdir $DOT + fi +} + + +function usage() { + cat >&2 <] [--password ] + [--token {|<@tokenfilename>}] + [--scopes ] login URL + forgejo-curl.sh logout + + OPTIONS + + --user username + --password password of + --scopes scopes of the token to be created (default ["all"]) + --token {|<@tokenfilename>} personal access token + + EXAMPLES + + forgejo-curl.sh --token ABCD \\ + login https://forgejo.example.com + + web is not authenticated + api, api_json use ABCD to authenticate + + forgejo-curl.sh --token @/tmp/token \\ + login https://forgejo.example.com + + web is not authenticated + api, api_json use the content of /tmp/token to authenticate + + forgejo-curl.sh --user joe --password passw0rd \\ + login https://forgejo.example.com + + web is authenticated + api, api_json use a newly generated token that belongs to user joe + with scope ["all"] to authenticate + + forgejo-curl.sh --user joe --password passw0rd --scopes '["write:package","write:issue"]' \\ + login https://forgejo.example.com + + web is authenticated + api, api_json use a newly generated token with write permission to packages and issues + to authenticate + +forgejo-curl.sh [--verbose] [--debug] web [curl options]" + + call curl using the CSRF token generated by the login command + + EXAMPLES + + forgejo-curl.sh web --form avatar=@avatar.png https://forgejo.example.com/settings/avatar + + upload the file avatar.png and update the avatar of the logged in user + +forgejo-curl.sh [--verbose] [--debug] api|api_json [curl options]" + + call curl using the token given to (or generated by) the login command. If called using + api_json, the Content-Type header is set to application/json. + + EXAMPLES + + forgejo-curl.sh api_json --data-raw '{"title":"TITLE"}' \\ + https://forgejo.example.com/api/v1/repos/joe/test/issues + + create a new issue in the repository test + + forgejo-curl.sh api --form name=image.png --form attachment=@image.png \\ + https://forgejo.example.com/api/v1/repos/joe/test/issues/1234/assets + + add the image.png file as an attachment to the issue 1234 in the test repository + +forgejo-curl.sh --help - display help +forgejo-curl.sh --version - show the version +EOF +} + +function main() { + local command=login user password token scopes + + while true; do + case "$1" in + --verbose) + shift + verbose + ;; + --debug) + shift + debug + ;; + --user) + shift + user="$1" + shift + ;; + --password) + shift + password="$1" + shift + ;; + --token) + shift + token="$1" + shift + ;; + --scopes) + shift + scopes="$1" + shift + ;; + login) + shift + login "$user" "$password" "$token" "$scopes" "$1" + return 0 + ;; + logout) + shift + logout + return 0 + ;; + web) + shift + web "$@" + return 0 + ;; + api) + shift + api "$@" + return 0 + ;; + api_json) + shift + api_json "$@" + return 0 + ;; + --version) + echo "forgejo-curl.sh version $VERSION" + return 0 + ;; + --help|*) + usage + return 1 + ;; + esac + done +} + +${MAIN:-main} "${@}" diff --git a/tests/run.sh b/tests/run.sh index 1e7dc4a..f6de5f8 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -2,46 +2,7 @@ # SPDX-License-Identifier: MIT SELF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -VERBOSE=false -DEBUG=false -: ${EXIT_ON_ERROR:=true} - -function debug() { - DEBUG=true - set -x - PS4='${BASH_SOURCE[0]}:$LINENO: ${FUNCNAME[0]}: ' -} - -function verbose() { - VERBOSE=true -} - -function log() { - echo "$@" >&2 -} - -function log_error() { - log "$@" -} - -function log_verbose() { - if $VERBOSE ; then - log "$@" - fi -} - -function log_info() { - log "$@" -} - -function fatal_error() { - log_error "$@" - if $EXIT_ON_ERROR ; then - exit 1 - else - return 1 - fi -} +source $SELF_DIR/../cascading-pr-lib.sh function push_self() { local host_port=$1 @@ -50,18 +11,16 @@ function push_self() { } function run() { - local host_port=$1 url=$2 token=$3 - - push_self $host_port + push_self ${options[host_port]} echo do something } function main() { local command=run - local host_port=$(cat forgejo-ip):3000 - local url=http://$host_port - local token=$(cat forgejo-token) + options[host_port]=$(cat forgejo-ip):3000 + options[url]=http://$host_port + options[token]=$(cat forgejo-token) while true; do case "$1" in @@ -75,18 +34,18 @@ function main() { ;; --host_port) shift - host_port=$1 + options[host_port]=$1 ;; --url) shift - url=$1 + options[url]=$1 ;; --token) shift - token=$1 + options[token]=$1 ;; *) - "${1:-run}" "$host_port" "$url" "$token" + "${1:-run}" return 0 ;; esac