Skip to content

Commit

Permalink
feat(scripts): Rewrite gen-upgrade-proposal.sh
Browse files Browse the repository at this point in the history
Fixes #8784

* script arguments for target release/tag/commit and Cosmos upgrade
  name, plus pass-through of arbitrary `agd tx` arguments
* automatic selection of upgrade name from gen-github-release.sh release
  contents, with interactive disambiguation (where possible) of multiple
  candidates
* reasonable defaults for title and description from
  gen-github-release.sh release contents
* script option `--send` to invoke `agd tx` rather than just printing it
  out (but successfully invocation additionally requires `--from` etc.)
* readable output of the `agd tx` command and data affecting it, whether
  or not it is to be invoked directly
* argument checking with readable error messages and usage details
* use of GitHub requests and https://main.agoric.net/network-config,
  overridable by environment variables
  • Loading branch information
gibson042 committed Dec 1, 2024
1 parent 23120a9 commit 82967c1
Showing 1 changed file with 275 additions and 33 deletions.
308 changes: 275 additions & 33 deletions scripts/gen-upgrade-proposal.sh
Original file line number Diff line number Diff line change
@@ -1,46 +1,288 @@
#! /bin/bash
set -ueo pipefail
declare -r USAGE='Usage: '"$0"' \
[<option>]... [--send] \
[<target ref>] \
[--] [<agd tx argument>]...
cat << 'EOF' 1>&2
------------------------------------------------------------------------
This script shows a command for a software upgrade proposal which is
compatible with the Cosmovisor found at:
https://github.com/agoric-labs/cosmos-sdk/tree/Agoric/cosmovisor#readme
------------------------------------------------------------------------
EOF
Generate and optionally issue an `agd tx` command for making a software-upgrade
governance proposal to bring the nodes of an Agoric blockchain up to the state
of GitHub repository Agoric/agoric-sdk at a particular release or commit
identified by checking the following in order:
1. [remote] Latest Release: If the specified target is "latest" and there is at
least one non-prerelease non-draft GitHub release, its target tag name.
2. [remote] Named Release: If the specified target is the name of a GitHub
release, its target tag name.
3. [remote] Remote Ref: If the specified target is a valid git rev such as a
commit ID or tag name or branch name, the commit currently identified by it.
4. [local] Local Ref: If the working directory is part of a git repository
(presumably agoric-sdk) and the specified target is a valid git rev, the
commit currently identified by it.
5. [local] Local Head: If no target is specified and the working directory is
part of a git repository, the commit currently identified by HEAD.
UPGRADE_TO="${1:-HEAD}"
COMMIT_ID=$(git rev-parse "$UPGRADE_TO")
ZIP_URL="https://github.com/Agoric/agoric-sdk/archive/${COMMIT_ID}.zip"
OPTIONS
-h, --help
Output this usage information.
-m {local|remote|any}, --mode={local|remote|any}
"local" omits [remote] checks; "remote" omits [local] checks.
Note that some required arguments (such as upgrade name) can only be
determined from a [remote] release created using gen-github-release.sh.
-u UPGRADE_NAME, --upgrade-name=UPGRADE_NAME
Force use of the specified upgrade name.
-s, --send
Invoke `agd tx` to send the proposal, rather than just printing out the
command to do so (additionally requires `--from` etc.).
ENVIRONMENT VARIABLES
GITHUB_API_URL specifies a substitute for "https://api.github.com".
GITHUB_SERVER_URL specifies a substitute for "https://github.com".
GITHUB_REPOSITORY specifies a substitute for "Agoric/agoric-sdk".
CONFIG_URL specifies a substitute for "https://main.agoric.net/network-config".
Past proposals for mainnet can be reviewed at https://ping.pub/agoric/gov
'
# usage [<exit code>]
usage () {
printf '%s' "$USAGE"
exit ${1:-64} # EX_USAGE from BSD sysexits
}
# fail <error message> [<exit code>]
fail () {
printf '%s\n\n' "$1"
usage "${2:-}"
} 1>&2
# q <string>
q () {
if [ -z "$1" ] || printf '%s' "$1" | grep -q '[^a-zA-Z0-9_,./:=-]'; then
printf '%s' "$1" | sed "s/'/'\\\\''/g; 1s/^/'/; \$s/\$/'/";
else
printf '%s' "$1"
fi
}

GH_API="${GITHUB_API_URL:-https://api.github.com}"
GH_URL="${GITHUB_SERVER_URL:-https://github.com}"
GH_REPO="${GITHUB_REPOSITORY:-Agoric/agoric-sdk}"
# gh_repo_curl /path/to/repo_resource
gh_repo_curl() {
curl -sSL \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"${GH_API}/repos/${GH_REPO}$1"
}
NET_CONFIG_URL="${CONFIG_URL:-https://main.agoric.net/network-config}"

echo "Verifying archive is at $ZIP_URL..." 1>&2
curl -fLI --no-progress-meter "$ZIP_URL" -o- > /dev/null

# Consume script arguments (but not agd arguments).
MODE=any
SEND=
FORCE_UPGRADE_NAME=
TARGET_REF=
ARG_COUNT=0
while [ $# -ge 1 ]; do
o="$1"
case "$1" in
--) shift; break ;;
-h*|--help) usage ;;
-m*|--mode=*|--mode)
# consume the argument, then push back a value from -x... or --xxx=...
shift
if [ -n "${o##--*}" ]; then
[ ${#o} -ge 3 ] && set -- "${o#-?}" "$@"
elif [ "${o#--*=}" != "$o" ]; then
set -- "${o#--*=}" "$@"
fi
# consume the value
[ $# -ge 1 ] || fail "$o requires a value"
case "$1" in
local|remote|any) MODE="$1" ;;
*) fail 'mode must be "local", "remote", or "any"' ;;
esac
shift
;;
-s*|--send)
# consume the argument, then push back any following [short] options
SEND=1
shift
if [ -n "${o##--*}" ]; then
[ ${#o} -ge 3 ] && set -- "-${o#-?}" "$@"
fi
;;
-u*|--upgrade-name=*|--upgrade-name)
# consume the argument, then push back a value from -x... or --xxx=...
shift
if [ -n "${o##--*}" ]; then
[ ${#o} -ge 3 ] && set -- "${o#-?}" "$@"
elif [ "${o#--*=}" != "$o" ]; then
set -- "${o#--*=}" "$@"
fi
[ $# -ge 1 ] || fail "$o requires a value"
FORCE_UPGRADE_NAME="$1"
shift
;;
-*) break ;;
*)
case "$ARG_COUNT" in
0) TARGET_REF="$1" ;;
*) fail 'Too many arguments' ;;
esac
ARG_COUNT=$((ARG_COUNT + 1))
shift
esac
done

# Determine target commit and (if possible) release name and known upgrade names.
COMMIT_ID=
RELEASE_NAME=
UPGRADE_NAMES=()
if [ -z "$COMMIT_ID" -a $MODE != local -a -n "$TARGET_REF" ]; then
echo "Checking $GH_API/repos/$GH_REPO for $TARGET_REF..." 1>&2
tag_name=
if [ "$TARGET_REF" = latest ]; then
path="/releases/latest"
else
path="/releases/tags/$TARGET_REF"
fi
release_json="$(gh_repo_curl "$path" || true)"
if [ -n "$release_json" ]; then
eval "$(printf '%s\n' "$release_json" | jq -r --arg q "'" --arg qsub "'\\''" '
[
["tag_name", .tag_name],
["RELEASE_NAME", .name],
(.body
| match("cosmos.*?name.*?: ([^\r\n]+)"; "gi")
| .captures[0].string
| ["UPGRADE_NAMES[${#UPGRADE_NAMES[@]}]", .]
)
]
| map("\(.[0])=\(.[1] | ($q + gsub($q; $qsub) + $q))")
| join("\n")
')"
fi
commit_json="$(gh_repo_curl "/commits/${tag_name:-$TARGET_REF}" || true)"
if [ -n "$commit_json" ]; then
COMMIT_ID="$(printf '%s\n' "$commit_json" | jq -r .sha)"
fi
fi
if [ -z "$COMMIT_ID" -a $MODE != remote ]; then
echo "Checking $(pwd) for ${TARGET_REF:-HEAD}..." 1>&2
COMMIT_ID="$(git rev-parse "${TARGET_REF:-HEAD}")"
fi
if [ -z "$COMMIT_ID" ]; then
echo "Could not identify a target commit" 1>&2
usage 1 | sed '/^$/q' 1>&2
fi

# Verify and digest the zipball.
ZIP_URL="$GH_URL/$GH_REPO/archive/${COMMIT_ID}.zip"
echo "Verifying archive at $ZIP_URL..." 1>&2
curl -fLI --no-progress-meter "$ZIP_URL" -o- > /dev/null
echo "Generating SHA-256 checksum..." 1>&2
CHECKSUM=sha256:$(curl -fL "$ZIP_URL" -o- | shasum -a 256 | cut -d' ' -f1)
printf '\n' 1>&2

# Generate upgrade-info compatible with Agoric Cosmovisor.
# https://github.com/agoric-labs/cosmos-sdk/tree/Agoric/cosmovisor#readme
BINARY_URL="$ZIP_URL//agoric-sdk-${COMMIT_ID}?checksum=$CHECKSUM"
SOURCE_URL="$ZIP_URL?checksum=$CHECKSUM"
UPGRADE_INFO="{\"binaries\":{\"any\":\"$BINARY_URL\"},\"source\":\"$SOURCE_URL\"}"

cat << 'EOF' 1>&2
------------------------------------------------------------------------
Here is the skeleton of the recomended upgrade proposal command.
You'll need to fill in the details and add arguments such as
`--chain-id=<chain id>` and `--from=<wallet>`.
Try `agd tx submit-proposal software-upgrade --help` for more info.
Also, take a look at existing on-chain software upgrade proposals for
examples: https://ping.pub/agoric/gov
------------------------------------------------------------------------
EOF

cat << EOF
agd tx submit-proposal software-upgrade <UPGRADE-NAME> \\
--upgrade-info='$UPGRADE_INFO' \\
--upgrade-height <HEIGHT> \\
--title '<TITLE>' \\
--description '<DESCRIPTION>' \\
<TXOPTS>...
EOF
# Determine the Cosmos upgrade name, prompting to resolve ambiguity (if possible).
UPGRADE_NAME=
if [ "${#UPGRADE_NAMES[@]}" -ge 1 ]; then
echo 'Found upgrade names:' 1>&2
for name in "${UPGRADE_NAMES[@]}"; do
printf '* %s\n' "$name" 1>&2
done
UPGRADE_NAME="${UPGRADE_NAMES[-1]}" # default to the last upgrade name
fi
if [ -n "$FORCE_UPGRADE_NAME" ]; then
UPGRADE_NAME="$FORCE_UPGRADE_NAME"
elif [ -t 0 -a -t 1 -a -t 2 -a "${#UPGRADE_NAMES[@]}" -ne 1 ]; then
hint="$(printf '%s' "$UPGRADE_NAME" | sed 's/\(..*\)/ (\1)/')"
found=0
printf '\n' 1>&2
while [ $found -eq 0 ]; do
printf 'upgrade name%s: ' "$hint" 1>&2
read UPGRADE_NAME
[ -n "$UPGRADE_NAME" ] || UPGRADE_NAME="${UPGRADE_NAMES[-1]}"
if [ -n "$UPGRADE_NAME" ]; then
found=$((${#UPGRADE_NAMES[@]} == 0))
for name in "${UPGRADE_NAMES[@]}"; do
[ -n "$name" -a "$UPGRADE_NAME" = "$name" ] && found=1
done
fi
if [ "$found" -eq 0 ]; then
printf 'use unknown upgrade name %s (y/n)? ' "$(q "$UPGRADE_NAME")"
read ok
case "$(printf '%s' "$ok" | tr A-Z a-z)" in
y|yes) found=1 ;;
esac
fi
done
elif [ -n "$UPGRADE_NAME" ]; then
printf 'Automatically selected %s.\n' "$(q "$UPGRADE_NAME")" 1>&2
else
echo "Could not identify an upgrade name" 1>&2
usage 1 | sed '/^$/q' 1>&2
fi

# Determine defaults for title and description.
DEFAULT_TITLE='<TITLE>'
DEFAULT_DESC='<DESCRIPTION>'
if [ -n "$RELEASE_NAME" ]; then
DEFAULT_TITLE="Upgrade to $RELEASE_NAME"
DEFAULT_DESC="$GH_URL/$GH_REPO/releases/tag/$RELEASE_NAME"
fi

# Determine which agd options are no-value flags.
flags="$(agd tx gov submit-proposal software-upgrade --help 2>&1 | awk '{
if (!sub(/^[[:space:]]*-/, "-")) next;
gsub(/,| +.*/, " ");
if (!match($0, / [^ -]/)) printf " %s ", $0;
}' || true)"
flags="${flags:-' --aux --dry-run --generate-only -h --help --ledger --no-validate --offline -y --yes --trace '}"

# Determine which agd options have been provided.
opts=' '
skip=
for arg in "$@"; do
[ -n "$skip" -o "${arg#-}" = "$arg" ] && skip= && continue
opts="$opts ${arg%%=*} " # exclude trailing `=...`
skip="${arg##*=*}" # skip next arg as an option value if $arg has no `=`
[ "${flags/ ${arg%%=*} /}" != "$flags" ] && skip= # ...and is not a flag
done

# Initialize the agd command with mandatory arguments.
get_chain_id="curl -sSL $NET_CONFIG_URL | jq -r .chainName"
get_height="agoric-estimator -date '<DATE>' -rpc https://main.rpc.agoric.net:443 | tee /dev/stderr | sed -n '\$s/.* //p'"
CMD="agd tx gov submit-proposal software-upgrade $(q "$UPGRADE_NAME") \\
--upgrade-info $(q "$UPGRADE_INFO") \\
$([ -z "${opts##* --title *}" ] || printf '%s %s' --title "$(q "$DEFAULT_TITLE")") \\
$([ -z "${opts##* --description *}" ] || printf '%s %s' --description "$(q "$DEFAULT_DESC")") \\
$([ -z "${opts##* --chain-id *}" ] || printf '%s "$(%s)"' --chain-id "$get_chain_id") \\
$([ -z "${opts##* --from *}" ] || echo "--from '<WALLET>'") \\
$([ -z "${opts##* --upgrade-height *}" ] || printf '%s "$(%s)"' --upgrade-height "$get_height") \\
"
CMD="$(echo "$CMD" | grep -v '^\( [\\]\|\)$' | sed '$s/ [\\]$//')"

# Incorporate remaining arguments.
skip=
for arg in "$@"; do
CMD="$CMD$(printf " $([ -n "$skip" ] || printf '%s' '\\\n ')%s" "$(q "$arg")")"
[ -n "$skip" -o "${arg#-}" = "$arg" ] && skip= && continue
skip="${arg##*=*}" # next arg is an option value if $arg has no `=`
[ "${flags/ ${arg%%=*} /}" != "$flags" ] && skip= # ...and is not a flag
done

# Print and optionally invoke the agd command.
printf '\n'
if [ -z "$SEND" ]; then
printf '%s\n\n' "$CMD"
else
printf '%s\n\n' "$CMD" 1>&2
echo "Executing..." 1>&2
eval "$CMD"
printf '\n'
fi

0 comments on commit 82967c1

Please sign in to comment.