mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-19 02:41:22 +00:00
added new auto-update scripts
This commit is contained in:
parent
87897d7db8
commit
6c0a9b3539
130
tools/auto-update-scripts/README.md
Normal file
130
tools/auto-update-scripts/README.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# Qortal Auto-Update Publisher Scripts
|
||||||
|
|
||||||
|
This toolkit modernizes and automates the Qortal auto-update process. It includes:
|
||||||
|
|
||||||
|
- A Bash script (`build-auto-update.sh`) to build and push the update
|
||||||
|
- A Python script (`publish-auto-update.py`) to publish the auto-update transaction
|
||||||
|
- Full support for dry-run mode, interactive or scripted use, and secure key input
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧰 Prerequisites
|
||||||
|
|
||||||
|
- You must be a **non-admin member** of the Qortal `dev` group
|
||||||
|
- A Qortal core node must be running locally (default API port: `12391`)
|
||||||
|
- You need the latest version of the `qortal` repo cloned locally
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Workflow Overview
|
||||||
|
|
||||||
|
### 1. Run the Build Script
|
||||||
|
|
||||||
|
This script:
|
||||||
|
- Auto-increments the version in `pom.xml`
|
||||||
|
- Rebuilds the JAR file
|
||||||
|
- XORs it into a `.update` file
|
||||||
|
- Creates a new `auto-update-<hash>` branch with only the update
|
||||||
|
- Pushes it to the repo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/auto-update-scripts/build-auto-update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll be prompted to:
|
||||||
|
- Confirm or modify the version number
|
||||||
|
- Push the version tag and update branch, and final commit.
|
||||||
|
- Optionally run the publisher script at the end
|
||||||
|
|
||||||
|
> ✅ Dry-run mode is supported to preview the full process.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Publish the Auto-Update
|
||||||
|
|
||||||
|
You can either:
|
||||||
|
- Let the build script call it for you
|
||||||
|
- Or run it manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run manually with interactive key prompt and auto-detected latest update:
|
||||||
|
python3 tools/auto-update-scripts/publish-auto-update.py
|
||||||
|
|
||||||
|
# Or specify a commit hash:
|
||||||
|
python3 tools/auto-update-scripts/publish-auto-update.py 0b37666d
|
||||||
|
|
||||||
|
# Or pass both from another script:
|
||||||
|
python3 tools/auto-update-scripts/publish-auto-update.py <privkey> <commit_hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
> 🔐 Private key is always prompted securely unless passed explicitly (e.g. from automation).
|
||||||
|
|
||||||
|
This script will:
|
||||||
|
- Detect the latest `auto-update-<hash>` branch (or use the one you specify)
|
||||||
|
- Validate that the commit exists
|
||||||
|
- Restore the `.update` file if missing
|
||||||
|
- Compute its SHA256 hash
|
||||||
|
- Build and sign the transaction
|
||||||
|
- Submit it to your local node
|
||||||
|
|
||||||
|
> ✅ `--dry-run` is supported to show what would happen without sending anything.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Advanced Options
|
||||||
|
|
||||||
|
- Log files are created in `~/qortal-auto-update-logs` by default
|
||||||
|
- You can override the log directory interactively
|
||||||
|
- Branch naming is standardized: `auto-update-<short-commit-hash>`
|
||||||
|
- The `.update` file is XOR-obfuscated using Qortal’s built-in logic
|
||||||
|
- Your commit must already exist on the main repo (e.g. via push or PR merge)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 Notes
|
||||||
|
|
||||||
|
- **Do not use Git LFS** — Qortal nodes download `.update` files using raw HTTP from GitHub
|
||||||
|
We may build LFS support in the future, but for now it is NOT utilized, and will NOT work.
|
||||||
|
(Other locations for the publish of the .update file will be utilized in the future,
|
||||||
|
preferably utilizing QDN via gateway nodes, until auto-update setup can be re-written to
|
||||||
|
leverage QDN directly.)
|
||||||
|
- GitHub will warn if `.update` files exceed 50MB, but auto-update still works.
|
||||||
|
(In the past there HAVE been issues with accounts getting banned due to publish of .update file,
|
||||||
|
however, as of recently (April 2025) it seems they are only warning, and not banning. But we
|
||||||
|
will be modifying the need for this in the future anyway.)
|
||||||
|
- Update mirrors will be added in the future, and others can be added in settings as well.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Example End-to-End (Manual)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/git-repos/qortal
|
||||||
|
./tools/auto-update-scripts/build-auto-update.sh
|
||||||
|
# follow prompts...
|
||||||
|
|
||||||
|
# then manually publish:
|
||||||
|
python3 tools/auto-update-scripts/publish-auto-update.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Test Without Sending
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build-auto-update.sh # enable dry-run when prompted
|
||||||
|
# OR
|
||||||
|
python3 publish-auto-update.py 0b37666d --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙌 Contributors
|
||||||
|
|
||||||
|
Modernization by [@crowetic](https://github.com/crowetic)
|
||||||
|
Based on original Perl scripts by Qortal core devs, specifically @catbref.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Questions or issues? Drop into the Qortal Dev group on Discord, Q-Chat, or reach out directly via Q-Mail to 'crowetic'.
|
||||||
|
|
264
tools/auto-update-scripts/build-auto-update.sh
Executable file
264
tools/auto-update-scripts/build-auto-update.sh
Executable file
@ -0,0 +1,264 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# === Configurable Defaults ===
|
||||||
|
BASE_BRANCH="master"
|
||||||
|
DEFAULT_LOG_DIR="${HOME}/qortal-auto-update-logs"
|
||||||
|
LOG_FILE=""
|
||||||
|
DRY_RUN=false
|
||||||
|
RUN_PUBLISH=false
|
||||||
|
PUBLISH_SCRIPT="tools/auto-update-scripts/publish-auto-update.py"
|
||||||
|
|
||||||
|
# === Helper Functions ===
|
||||||
|
function abort() {
|
||||||
|
echo -e "\nERROR: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm_or_exit() {
|
||||||
|
echo "$1"
|
||||||
|
read -rp "Continue? (y/N): " confirm
|
||||||
|
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_git() {
|
||||||
|
echo "Running: git $*" | tee -a "$LOG_FILE"
|
||||||
|
$DRY_RUN || git "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
function increment_version() {
|
||||||
|
local version=$1
|
||||||
|
local major minor patch
|
||||||
|
IFS='.' read -r major minor patch <<< "$version"
|
||||||
|
((patch++))
|
||||||
|
echo "$major.$minor.$patch"
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Prompt for Logging Directory ===
|
||||||
|
echo "Default log directory: ${DEFAULT_LOG_DIR}"
|
||||||
|
read -rp "Use this log directory? (Y/n): " log_choice
|
||||||
|
if [[ "${log_choice}" =~ ^[Nn]$ ]]; then
|
||||||
|
read -rp "Enter desired log directory path: " CUSTOM_LOG_DIR
|
||||||
|
LOG_DIR="${CUSTOM_LOG_DIR}"
|
||||||
|
else
|
||||||
|
LOG_DIR="${DEFAULT_LOG_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${LOG_DIR}" || abort "Unable to create log directory: ${LOG_DIR}"
|
||||||
|
LOG_FILE="${LOG_DIR}/qortal-auto-update-log-$(date +%Y%m%d-%H%M%S).log"
|
||||||
|
echo "Logging to: ${LOG_FILE}"
|
||||||
|
|
||||||
|
# Log everything to file as well as terminal
|
||||||
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
|
|
||||||
|
# === Dry Run Mode Option ===
|
||||||
|
read -rp "Enable dry-run mode? (y/N): " dry_choice
|
||||||
|
if [[ "${dry_choice}" =~ ^[Yy]$ ]]; then
|
||||||
|
DRY_RUN=true
|
||||||
|
echo "Dry-run mode ENABLED. Commands will be shown but not executed."
|
||||||
|
else
|
||||||
|
echo "Dry-run mode DISABLED. Real commands will be executed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Run Python Publisher Option ===
|
||||||
|
read -rp "Run the Python publish_auto_update script at the end? (y/N): " pub_choice
|
||||||
|
if [[ "${pub_choice}" =~ ^[Yy]$ ]]; then
|
||||||
|
RUN_PUBLISH=true
|
||||||
|
read -rp "Run Python script in dry-run mode? (y/N): " pub_dry
|
||||||
|
if [[ "${pub_dry}" =~ ^[Yy]$ ]]; then
|
||||||
|
PUBLISH_DRY_FLAG="--dry-run"
|
||||||
|
else
|
||||||
|
PUBLISH_DRY_FLAG=""
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
RUN_PUBLISH=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Detect Git Root ===
|
||||||
|
git_dir=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
[[ -z "${git_dir}" ]] && abort "Not inside a git repository."
|
||||||
|
cd "${git_dir}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Current Git identity:"
|
||||||
|
git config user.name || echo "(not set)"
|
||||||
|
git config user.email || echo "(not set)"
|
||||||
|
|
||||||
|
read -rp "Would you like to set/override the Git username and email for this repo? (y/N): " git_id_choice
|
||||||
|
if [[ "${git_id_choice}" =~ ^[Yy]$ ]]; then
|
||||||
|
read -rp "Enter Git username (e.g. Qortal-Auto-Update): " git_user
|
||||||
|
read -rp "Enter Git email (e.g. qortal-auto-update@example.com): " git_email
|
||||||
|
|
||||||
|
run_git config user.name "${git_user}"
|
||||||
|
run_git config user.email "${git_email}"
|
||||||
|
echo "Git identity set to: ${git_user} <${git_email}>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Confirm Git Origin URL ===
|
||||||
|
git_origin=$(git config --get remote.origin.url)
|
||||||
|
echo "Git origin URL: ${git_origin}"
|
||||||
|
confirm_or_exit "Is this the correct repository?"
|
||||||
|
|
||||||
|
# === Verify Current Branch ===
|
||||||
|
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
echo "Current git branch: ${current_branch}"
|
||||||
|
if [[ "${current_branch}" != "${BASE_BRANCH}" ]]; then
|
||||||
|
echo "Expected to be on '${BASE_BRANCH}' branch, but found '${current_branch}'"
|
||||||
|
confirm_or_exit "Proceed anyway in 5 seconds or abort with CTRL+C."
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Check for Uncommitted Changes ===
|
||||||
|
uncommitted=$(git status --short --untracked-files=no)
|
||||||
|
if [[ -n "${uncommitted}" ]]; then
|
||||||
|
echo "Uncommitted changes detected:"
|
||||||
|
echo "${uncommitted}"
|
||||||
|
abort "Please commit or stash changes first."
|
||||||
|
fi
|
||||||
|
|
||||||
|
project=$(grep -oPm1 "(?<=<artifactId>)[^<]+" pom.xml)
|
||||||
|
[[ -z "${project}" ]] && abort "Unable to determine project name from pom.xml."
|
||||||
|
echo "Detected project: ${project}"
|
||||||
|
|
||||||
|
# === Auto-Increment Version in pom.xml ===
|
||||||
|
current_version=$(grep -oPm1 "(?<=<version>)[^<]+" pom.xml)
|
||||||
|
new_version=$(increment_version "$current_version")
|
||||||
|
|
||||||
|
$DRY_RUN || sed -i "s|<version>${current_version}</version>|<version>${new_version}</version>|" pom.xml
|
||||||
|
|
||||||
|
echo "Updated version from ${current_version} to ${new_version} in pom.xml"
|
||||||
|
git diff pom.xml
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
read -rp "Is the updated version correct? (y/N): " version_ok
|
||||||
|
if [[ "${version_ok}" =~ ^[Yy]$ ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -rp "Enter the correct version number (e.g., 4.7.2): " user_version
|
||||||
|
|
||||||
|
# Validate format x.y.z and version > current_version
|
||||||
|
if [[ ! "${user_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "Invalid format. Use x.y.z (e.g., 4.7.2)."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS='.' read -r curr_major curr_minor curr_patch <<< "${current_version}"
|
||||||
|
IFS='.' read -r new_major new_minor new_patch <<< "${user_version}"
|
||||||
|
|
||||||
|
if (( new_major < curr_major )) || \
|
||||||
|
(( new_major == curr_major && new_minor < curr_minor )) || \
|
||||||
|
(( new_major == curr_major && new_minor == curr_minor && new_patch <= curr_patch )); then
|
||||||
|
echo "Version must be greater than current version (${current_version})."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
$DRY_RUN || sed -i "s|<version>${new_version}</version>|<version>${user_version}</version>|" pom.xml
|
||||||
|
echo "Updated version to user-provided version: ${user_version}"
|
||||||
|
git diff pom.xml
|
||||||
|
new_version="${user_version}"
|
||||||
|
echo
|
||||||
|
echo "Rechecking updated version..."
|
||||||
|
done
|
||||||
|
|
||||||
|
run_git add pom.xml
|
||||||
|
run_git commit -m "Bump version to ${new_version}"
|
||||||
|
run_git tag "v${new_version}"
|
||||||
|
confirm_or_exit "About to push version tag 'v${new_version}' to origin."
|
||||||
|
run_git push origin "v${new_version}"
|
||||||
|
confirm_or_exit "Also push the ${current_branch} branch to origin?"
|
||||||
|
run_git push origin "${current_branch}"
|
||||||
|
|
||||||
|
# === Extract Info ===
|
||||||
|
short_hash=$(git rev-parse --short HEAD)
|
||||||
|
[[ -z "${short_hash}" ]] && abort "Unable to extract commit hash."
|
||||||
|
echo "Using commit hash: ${short_hash}"
|
||||||
|
|
||||||
|
|
||||||
|
# === Build JAR ===
|
||||||
|
echo "Building JAR for ${project}..."
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
mvn clean package > /dev/null 2>&1 || {
|
||||||
|
echo "Build failed. Check logs in ${LOG_FILE}" >&2
|
||||||
|
abort "Maven build failed."
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
jar_file=$(ls target/${project}*.jar | head -n1)
|
||||||
|
[[ ! -f "${jar_file}" ]] && abort "Built JAR file not found."
|
||||||
|
|
||||||
|
# === XOR Obfuscation ===
|
||||||
|
echo "Creating ${project}.update..."
|
||||||
|
$DRY_RUN || java -cp "${jar_file}" org.qortal.XorUpdate "${jar_file}" "${project}.update"
|
||||||
|
|
||||||
|
# === Create Auto-Update Branch ===
|
||||||
|
update_branch="auto-update-${short_hash}"
|
||||||
|
|
||||||
|
echo "Creating update branch: ${update_branch}"
|
||||||
|
if git show-ref --verify --quiet refs/heads/${update_branch}; then
|
||||||
|
run_git branch -D "${update_branch}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_git checkout --orphan "${update_branch}"
|
||||||
|
$DRY_RUN || git rm -rf . > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
run_git add "${project}.update"
|
||||||
|
run_git commit -m "XORed auto-update JAR for commit ${short_hash}"
|
||||||
|
|
||||||
|
confirm_or_exit "About to push auto-update branch '${update_branch}' to origin."
|
||||||
|
run_git push --set-upstream origin "${update_branch}"
|
||||||
|
|
||||||
|
# === Return to Original Branch ===
|
||||||
|
echo "Switching back to original branch: ${current_branch}"
|
||||||
|
run_git checkout --force "${current_branch}"
|
||||||
|
echo "Done. ${project}.update is committed to ${update_branch}."
|
||||||
|
|
||||||
|
# === Summary Output ===
|
||||||
|
echo
|
||||||
|
echo "======================================"
|
||||||
|
echo "✅ Auto-Update Build Complete!"
|
||||||
|
echo "--------------------------------------"
|
||||||
|
echo "Project: ${project}"
|
||||||
|
echo "Version: ${new_version}"
|
||||||
|
echo "Tag: v${new_version}"
|
||||||
|
echo "Commit Hash: ${short_hash}"
|
||||||
|
echo "Auto-Update Branch: auto-update-${short_hash}"
|
||||||
|
echo
|
||||||
|
echo "Pushed to: ${git_origin}"
|
||||||
|
echo "Logs saved to: ${LOG_FILE}"
|
||||||
|
echo "======================================"
|
||||||
|
echo
|
||||||
|
# === Provide additional information regarding publish script, and private key. ===
|
||||||
|
if $RUN_PUBLISH; then
|
||||||
|
echo "...===...===...===...===...===..."
|
||||||
|
echo
|
||||||
|
echo "CONTINUING TO EXECUTE PUBLISH SCRIPT AS SELECTED"
|
||||||
|
echo
|
||||||
|
echo "This will publish the AUTO-UPDATE TRANSACTION for signing by the DEVELOPER GROUP ADMINS"
|
||||||
|
echo
|
||||||
|
echo "NOTICE: For security, when prompted for PRIVATE KEY, you will NOT see the input, SIMPLY PASTE/TYPE KEY AND PUSH ENTER."
|
||||||
|
echo
|
||||||
|
echo "...===...===...===...===...===..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Optionally Run Python Publisher ===
|
||||||
|
if $RUN_PUBLISH; then
|
||||||
|
echo "Running Python publish_auto_update script..."
|
||||||
|
if [[ -f "${PUBLISH_SCRIPT}" ]]; then
|
||||||
|
read -rsp "Enter your Base58 private key: " PRIVATE_KEY
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ "${PUBLISH_DRY_FLAG}" == "--dry-run" ]]; then
|
||||||
|
echo "Dry-run mode active for Python script."
|
||||||
|
python3 "${PUBLISH_SCRIPT}" "${PRIVATE_KEY}" "${short_hash}" --dry-run
|
||||||
|
else
|
||||||
|
echo "Publishing auto-update for real..."
|
||||||
|
python3 "${PUBLISH_SCRIPT}" "${PRIVATE_KEY}" "${short_hash}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WARNING: Python script not found at ${PUBLISH_SCRIPT}. Skipping."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
238
tools/auto-update-scripts/generate-release-notes.sh
Executable file
238
tools/auto-update-scripts/generate-release-notes.sh
Executable file
@ -0,0 +1,238 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Check if version argument is passed
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Usage: $0 <VERSION>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VERSION="$1"
|
||||||
|
# Repository and branch information
|
||||||
|
REPO="Qortal/qortal"
|
||||||
|
BRANCH="master"
|
||||||
|
WORKING_QORTAL_DIR='./qortal'
|
||||||
|
|
||||||
|
# 1. Check if working directory exists
|
||||||
|
if [ ! -d "$WORKING_QORTAL_DIR" ]; then
|
||||||
|
echo "Error: Working directory '$WORKING_QORTAL_DIR' not found."
|
||||||
|
read -p "Would you like to: (1) Create a new directory here, or (2) Specify a full path? [1/2]: " choice
|
||||||
|
if [ "$choice" = "1" ]; then
|
||||||
|
mkdir -p "$WORKING_QORTAL_DIR"
|
||||||
|
echo "Created new directory: $WORKING_QORTAL_DIR"
|
||||||
|
elif [ "$choice" = "2" ]; then
|
||||||
|
read -p "Enter full path to working directory: " new_path
|
||||||
|
WORKING_QORTAL_DIR="$new_path"
|
||||||
|
echo "Using specified directory: $WORKING_QORTAL_DIR"
|
||||||
|
else
|
||||||
|
echo "Invalid choice. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Check for qortal.jar
|
||||||
|
JAR_FILE="$WORKING_QORTAL_DIR/qortal.jar"
|
||||||
|
if [ ! -f "$JAR_FILE" ]; then
|
||||||
|
echo "Error: $JAR_FILE not found."
|
||||||
|
read -p "Would you like to: (1) Compile from source, (2) Use running qortal.jar, or (3) Specify a path? [1/2/3]: " choice
|
||||||
|
if [ "$choice" = "1" ]; then
|
||||||
|
echo "Cloning repo and compiling..."
|
||||||
|
git clone https://github.com/Qortal/qortal.git /tmp/qortal
|
||||||
|
if ! command -v mvn &> /dev/null; then
|
||||||
|
echo "Error: Maven not found. Please install Maven and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cd /tmp/qortal || exit
|
||||||
|
mvn clean package
|
||||||
|
cp target/qortal-*.jar "$WORKING_QORTAL_DIR/qortal.jar"
|
||||||
|
cd - || exit
|
||||||
|
elif [ "$choice" = "2" ]; then
|
||||||
|
if [ -f "$HOME/qortal/qortal.jar" ]; then
|
||||||
|
cp "$HOME/qortal/qortal.jar" "$WORKING_QORTAL_DIR/"
|
||||||
|
echo "Copied from $HOME/qortal/qortal.jar"
|
||||||
|
else
|
||||||
|
echo "Error: $HOME/qortal/qortal.jar not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ "$choice" = "3" ]; then
|
||||||
|
read -p "Enter full path to qortal.jar: " jar_path
|
||||||
|
cp "$jar_path" "$WORKING_QORTAL_DIR/"
|
||||||
|
echo "Used specified path: $jar_path"
|
||||||
|
else
|
||||||
|
echo "Invalid choice. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Check for required files (settings.json, log4j2.properties, etc.)
|
||||||
|
REQUIRED_FILES=("settings.json" "log4j2.properties" "start.sh" "stop.sh" "qort")
|
||||||
|
for file in "${REQUIRED_FILES[@]}"; do
|
||||||
|
if [ ! -f "$WORKING_QORTAL_DIR/$file" ]; then
|
||||||
|
echo "Error: $WORKING_QORTAL_DIR/$file not found."
|
||||||
|
read -p "Would you like to: (1) Get files from GitHub (2) exit and copy files manually then re-run? [1/2]: " choice
|
||||||
|
if [ "$choice" = "1" ]; then
|
||||||
|
if [ "$file" = "settings.json" ]; then
|
||||||
|
cat <<EOF > "$WORKING_QORTAL_DIR/settings.json"
|
||||||
|
{
|
||||||
|
"balanceRecorderEnabled": true,
|
||||||
|
"apiWhitelistEnabled": false,
|
||||||
|
"allowConnectionsWithOlderPeerVersions": false,
|
||||||
|
"apiRestricted": false
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
elif [ "${file}" = "qort" ]; then
|
||||||
|
echo "Downloading from GitHub..."
|
||||||
|
curl -s "https://raw.githubusercontent.com/Qortal/qortal/refs/heads/$BRANCH/tools/$file" -o "$WORKING_QORTAL_DIR/$file"
|
||||||
|
echo "Making $file script executable..."
|
||||||
|
chmod +x "$WORKING_QORTAL_DIR/$file"
|
||||||
|
elif [ "${file}" = "start.sh" ]; then
|
||||||
|
echo "Downloading from GitHub..."
|
||||||
|
curl -s "https://raw.githubusercontent.com/Qortal/qortal/refs/heads/$BRANCH/$file" -o "$WORKING_QORTAL_DIR/$file"
|
||||||
|
echo "Making $file script executable..."
|
||||||
|
chmod +x "$WORKING_QORTAL_DIR/$file"
|
||||||
|
elif [ "${file}" = "stop.sh" ]; then
|
||||||
|
echo "Downloading from GitHub..."
|
||||||
|
curl -s "https://raw.githubusercontent.com/Qortal/qortal/refs/heads/$BRANCH/$file" -o "$WORKING_QORTAL_DIR/$file"
|
||||||
|
echo "Making $file script executable..."
|
||||||
|
chmod +x "$WORKING_QORTAL_DIR/$file"
|
||||||
|
else
|
||||||
|
echo "Downloading from GitHub..."
|
||||||
|
curl -s "https://raw.githubusercontent.com/Qortal/qortal/refs/heads/$BRANCH/$file" -o "$WORKING_QORTAL_DIR/$file"
|
||||||
|
fi
|
||||||
|
elif [ "$choice" = "2" ]; then
|
||||||
|
echo "copy files manually to this location then re-run script..."
|
||||||
|
sleep 5
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Invalid choice. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Continue with the rest of the script...
|
||||||
|
# (The rest of the script remains unchanged)
|
||||||
|
|
||||||
|
# Fetch the latest 100 commits
|
||||||
|
COMMITS_JSON=$(curl -s "https://api.github.com/repos/${REPO}/commits?sha=${BRANCH}&per_page=100")
|
||||||
|
|
||||||
|
# Extract bump version commits
|
||||||
|
BUMP_COMMITS=$(echo "$COMMITS_JSON" | jq -r '.[] | select(.commit.message | test("bump version to"; "i")) | .sha')
|
||||||
|
|
||||||
|
CURRENT_BUMP_COMMIT=$(echo "$COMMITS_JSON" | jq -r ".[] | select(.commit.message | test(\"bump version to ${VERSION}\"; \"i\")) | .sha" | head -n1)
|
||||||
|
PREV_BUMP_COMMIT=$(echo "$BUMP_COMMITS" | sed -n '2p')
|
||||||
|
|
||||||
|
if [ -z "$CURRENT_BUMP_COMMIT" ]; then
|
||||||
|
echo "Error: Could not find bump commit for version ${VERSION} in ${REPO}/${BRANCH}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get changelog between previous and current commit
|
||||||
|
echo "Generating changelog between ${PREV_BUMP_COMMIT} and ${CURRENT_BUMP_COMMIT}..."
|
||||||
|
CHANGELOG=$(curl -s "https://api.github.com/repos/${REPO}/compare/${PREV_BUMP_COMMIT}...${CURRENT_BUMP_COMMIT}" | jq -r '.commits[] | "- " + .sha[0:7] + " " + .commit.message')
|
||||||
|
|
||||||
|
# Fetch latest commit timestamp from GitHub API for final file timestamping
|
||||||
|
COMMIT_API_URL="https://api.github.com/repos/${REPO}/commits?sha=${BRANCH}&per_page=1"
|
||||||
|
COMMIT_TIMESTAMP=$(curl -s "${COMMIT_API_URL}" | jq -r '.[0].commit.committer.date')
|
||||||
|
|
||||||
|
if [ -z "${COMMIT_TIMESTAMP}" ] || [ "${COMMIT_TIMESTAMP}" == "null" ]; then
|
||||||
|
echo "Error: Unable to retrieve the latest commit timestamp from GitHub API."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Define file names
|
||||||
|
JAR_FILE="qortal/qortal.jar"
|
||||||
|
EXE_FILE="qortal.exe"
|
||||||
|
ZIP_FILE="qortal.zip"
|
||||||
|
|
||||||
|
calculate_hashes() {
|
||||||
|
local file="$1"
|
||||||
|
echo "Calculating hashes for ${file}..."
|
||||||
|
MD5=$(md5sum "${file}" | awk '{print $1}')
|
||||||
|
SHA1=$(sha1sum "${file}" | awk '{print $1}')
|
||||||
|
SHA256=$(sha256sum "${file}" | awk '{print $1}')
|
||||||
|
echo "MD5: ${MD5}, SHA1: ${SHA1}, SHA256: ${SHA256}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hashes for qortal.jar
|
||||||
|
if [ -f "${JAR_FILE}" ]; then
|
||||||
|
calculate_hashes "${JAR_FILE}"
|
||||||
|
JAR_MD5=${MD5}
|
||||||
|
JAR_SHA1=${SHA1}
|
||||||
|
JAR_SHA256=${SHA256}
|
||||||
|
else
|
||||||
|
echo "Error: ${JAR_FILE} not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Hashes for qortal.exe
|
||||||
|
if [ -f "${EXE_FILE}" ]; then
|
||||||
|
calculate_hashes "${EXE_FILE}"
|
||||||
|
EXE_MD5=${MD5}
|
||||||
|
EXE_SHA1=${SHA1}
|
||||||
|
EXE_SHA256=${SHA256}
|
||||||
|
else
|
||||||
|
echo "Warning: ${EXE_FILE} not found. Skipping."
|
||||||
|
EXE_MD5="<INPUT>"
|
||||||
|
EXE_SHA1="<INPUT>"
|
||||||
|
EXE_SHA256="<INPUT>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apply commit timestamp to files in qortal/
|
||||||
|
echo "Applying commit timestamp (${COMMIT_TIMESTAMP}) to files..."
|
||||||
|
mv qortal.exe ${WORKING_QORTAL_DIR} 2>/dev/null || true
|
||||||
|
find ${WORKING_QORTAL_DIR} -type f -exec touch -d "${COMMIT_TIMESTAMP}" {} \;
|
||||||
|
mv ${WORKING_QORTAL_DIR}/qortal.exe . 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create qortal.zip
|
||||||
|
echo "Packing ${ZIP_FILE}..."
|
||||||
|
7z a -r -tzip "${ZIP_FILE}" ${WORKING_QORTAL_DIR}/ -stl
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to create ${ZIP_FILE}."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
calculate_hashes "${ZIP_FILE}"
|
||||||
|
ZIP_MD5=${MD5}
|
||||||
|
ZIP_SHA1=${SHA1}
|
||||||
|
ZIP_SHA256=${SHA256}
|
||||||
|
|
||||||
|
# Generate release notes
|
||||||
|
cat <<EOF > release-notes.txt
|
||||||
|
### **_Qortal Core V${VERSION}_**
|
||||||
|
|
||||||
|
#### 🔄 Changes Included in This Release:
|
||||||
|
|
||||||
|
${CHANGELOG}
|
||||||
|
|
||||||
|
### [qortal.jar](https://github.com/Qortal/qortal/releases/download/v${VERSION}/qortal.jar)
|
||||||
|
|
||||||
|
\`MD5: ${JAR_MD5}\` qortal.jar
|
||||||
|
\`SHA1: ${JAR_SHA1}\` qortal.jar
|
||||||
|
\`SHA256: ${JAR_SHA256}\` qortal.jar
|
||||||
|
|
||||||
|
### [qortal.exe](https://github.com/Qortal/qortal/releases/download/v${VERSION}/qortal.exe)
|
||||||
|
|
||||||
|
\`MD5: ${EXE_MD5}\` qortal.exe
|
||||||
|
\`SHA1: ${EXE_SHA1}\` qortal.exe
|
||||||
|
\`SHA256: ${EXE_SHA256}\` qortal.exe
|
||||||
|
|
||||||
|
[VirusTotal report for qortal.exe](https://www.virustotal.com/gui/file/${EXE_SHA256}/detection)
|
||||||
|
|
||||||
|
### [qortal.zip](https://github.com/Qortal/qortal/releases/download/v${VERSION}/qortal.zip)
|
||||||
|
|
||||||
|
Contains bare minimum of:
|
||||||
|
* built \`qortal.jar\`
|
||||||
|
* \`log4j2.properties\` from git repo
|
||||||
|
* \`start.sh\` from git repo
|
||||||
|
* \`stop.sh\` from git repo
|
||||||
|
* \`qort\` script for linux/mac easy API utilization
|
||||||
|
* \`printf "{\n}\n" > settings.json\`
|
||||||
|
|
||||||
|
All timestamps set to same date-time as commit.
|
||||||
|
Packed with \`7z a -r -tzip qortal.zip qortal/\`
|
||||||
|
|
||||||
|
\`MD5: ${ZIP_MD5}\` qortal.zip
|
||||||
|
\`SHA1: ${ZIP_SHA1}\` qortal.zip
|
||||||
|
\`SHA256: ${ZIP_SHA256}\` qortal.zip
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Release notes generated: release-notes.txt"
|
||||||
|
|
234
tools/auto-update-scripts/publish-auto-update.py
Executable file
234
tools/auto-update-scripts/publish-auto-update.py
Executable file
@ -0,0 +1,234 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def run(cmd, cwd=None, capture_output=True):
|
||||||
|
result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=capture_output, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Command failed: {cmd}\n{result.stderr}")
|
||||||
|
sys.exit(1)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
def get_project_name():
|
||||||
|
pom = Path('pom.xml')
|
||||||
|
if not pom.exists():
|
||||||
|
sys.exit("pom.xml not found!")
|
||||||
|
for line in pom.read_text().splitlines():
|
||||||
|
if '<artifactId>' in line:
|
||||||
|
return line.strip().split('>')[1].split('<')[0]
|
||||||
|
sys.exit("artifactId not found in pom.xml")
|
||||||
|
|
||||||
|
def get_commit_info(commit_hash=None, dry_run=False):
|
||||||
|
if not commit_hash:
|
||||||
|
print("No commit hash provided, detecting most recent auto-update branch...")
|
||||||
|
run("git fetch origin") # Ensure up-to-date
|
||||||
|
|
||||||
|
# Get latest auto-update branch by commit date
|
||||||
|
branches = run("git for-each-ref --sort=-committerdate --format='%(refname:short)' refs/remotes/origin/")
|
||||||
|
for branch in branches.splitlines():
|
||||||
|
branch = branch.strip().strip("'")
|
||||||
|
if branch.startswith("origin/auto-update-"):
|
||||||
|
commit_hash = branch.replace("origin/auto-update-", "")
|
||||||
|
print(f"Found latest auto-update branch: {branch}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not commit_hash:
|
||||||
|
sys.exit("No auto-update branches found.")
|
||||||
|
|
||||||
|
# Validate and get timestamp
|
||||||
|
if not commit_exists(commit_hash):
|
||||||
|
sys.exit(f"Commit hash '{commit_hash}' does not exist.")
|
||||||
|
|
||||||
|
timestamp = int(run(f"git show --no-patch --format=%ct {commit_hash}")) * 1000
|
||||||
|
|
||||||
|
# Use the remote branch hash if available
|
||||||
|
try:
|
||||||
|
update_hash = run(f"git rev-parse origin/auto-update-{commit_hash}")
|
||||||
|
except SystemExit:
|
||||||
|
print(f"⚠️ Warning: remote branch origin/auto-update-{commit_hash} not found, using commit hash itself.")
|
||||||
|
update_hash = run(f"git rev-parse {commit_hash}")
|
||||||
|
|
||||||
|
return commit_hash, timestamp, update_hash
|
||||||
|
|
||||||
|
|
||||||
|
def commit_exists(commit_hash):
|
||||||
|
try:
|
||||||
|
run(f"git cat-file -t {commit_hash}")
|
||||||
|
return True
|
||||||
|
except SystemExit:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_sha256(update_file_path):
|
||||||
|
sha256 = hashlib.sha256()
|
||||||
|
with open(update_file_path, 'rb') as f:
|
||||||
|
sha256.update(f.read())
|
||||||
|
return sha256.hexdigest()
|
||||||
|
|
||||||
|
def get_public_key(base58_privkey, port):
|
||||||
|
r = requests.post(f"http://localhost:{port}/utils/publickey", data=base58_privkey)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
def get_hex_key(base58_key, port):
|
||||||
|
r = requests.post(f"http://localhost:{port}/utils/frombase58", data=base58_key)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
def get_address(pubkey, port):
|
||||||
|
r = requests.get(f"http://localhost:{port}/addresses/convert/{pubkey}")
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
def get_reference(address, port):
|
||||||
|
r = requests.get(f"http://localhost:{port}/addresses/lastreference/{address}")
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
def to_base58(hex_str, port):
|
||||||
|
r = requests.get(f"http://localhost:{port}/utils/tobase58/{hex_str}")
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
def sign_transaction(privkey, tx_base58, port):
|
||||||
|
payload = json.dumps({"privateKey": privkey, "transactionBytes": tx_base58})
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
r = requests.post(f"http://localhost:{port}/transactions/sign", data=payload, headers=headers)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
def process_transaction(signed_tx, port):
|
||||||
|
r = requests.post(f"http://localhost:{port}/transactions/process", data=signed_tx)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text == 'true'
|
||||||
|
|
||||||
|
def decode_transaction(signed_tx, port):
|
||||||
|
r = requests.post(f"http://localhost:{port}/transactions/decode", data=signed_tx, headers={"Content-Type": "application/json"})
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import getpass
|
||||||
|
parser = argparse.ArgumentParser(description="Modern auto-update publisher for Qortal")
|
||||||
|
parser.add_argument("arg1", nargs="?", help="Private key OR commit hash")
|
||||||
|
parser.add_argument("arg2", nargs="?", help="Commit hash if arg1 was private key")
|
||||||
|
parser.add_argument("--port", type=int, default=12391, help="API port")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="Simulate without submitting transaction")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Handle combinations
|
||||||
|
if args.arg1 and args.arg2:
|
||||||
|
privkey = args.arg1
|
||||||
|
commit_hash = args.arg2
|
||||||
|
elif args.arg1 and not args.arg2:
|
||||||
|
commit_hash = args.arg1
|
||||||
|
privkey = getpass.getpass("Enter your Base58 private key: ")
|
||||||
|
else:
|
||||||
|
commit_hash = None # Will auto-resolve from HEAD
|
||||||
|
privkey = getpass.getpass("Enter your Base58 private key: ")
|
||||||
|
|
||||||
|
# Switch to repo root
|
||||||
|
git_root = run("git rev-parse --show-toplevel")
|
||||||
|
os.chdir(git_root)
|
||||||
|
|
||||||
|
project = get_project_name()
|
||||||
|
|
||||||
|
# Resolve and verify commit
|
||||||
|
commit_hash, timestamp, update_hash = get_commit_info(commit_hash, args.dry_run)
|
||||||
|
if not commit_exists(commit_hash):
|
||||||
|
sys.exit(f"Commit hash '{commit_hash}' does not exist in this repo.")
|
||||||
|
|
||||||
|
print(f"Commit: {commit_hash}, Timestamp: {timestamp}, Auto-update hash: {update_hash}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_sha256(update_file_path):
|
||||||
|
sha256 = hashlib.sha256()
|
||||||
|
with open(update_file_path, 'rb') as f:
|
||||||
|
sha256.update(f.read())
|
||||||
|
return sha256.hexdigest()
|
||||||
|
|
||||||
|
update_file = Path(f"{project}.update")
|
||||||
|
|
||||||
|
if not update_file.exists():
|
||||||
|
print(f"{project}.update not found locally. Attempting to restore from branch auto-update-{commit_hash}...")
|
||||||
|
try:
|
||||||
|
restore_cmd = f"git show auto-update-{commit_hash}:{project}.update > {project}.update"
|
||||||
|
run(restore_cmd)
|
||||||
|
print(f"✓ Restored {project}.update from branch auto-update-{commit_hash}")
|
||||||
|
except Exception as e:
|
||||||
|
sys.exit(f"Failed to restore {project}.update: {e}")
|
||||||
|
|
||||||
|
# Final check to ensure the file was restored
|
||||||
|
if not update_file.exists():
|
||||||
|
sys.exit(f"{project}.update still not found after attempted restore")
|
||||||
|
|
||||||
|
|
||||||
|
sha256 = get_sha256(update_file)
|
||||||
|
print(f"Update SHA256: {sha256}")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("\n--- DRY RUN ---")
|
||||||
|
print(f"Would use timestamp: {timestamp}")
|
||||||
|
print(f"Would use update hash: {update_hash}")
|
||||||
|
print(f"Would use SHA256: {sha256}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
pubkey = get_public_key(privkey, args.port)
|
||||||
|
pubkey_hex = get_hex_key(pubkey, args.port)
|
||||||
|
address = get_address(pubkey, args.port)
|
||||||
|
reference = get_reference(address, args.port)
|
||||||
|
reference_hex = get_hex_key(reference, args.port)
|
||||||
|
|
||||||
|
data_hex = f"{timestamp:016x}{update_hash}{sha256}"
|
||||||
|
if len(data_hex) != 120:
|
||||||
|
sys.exit("Data hex length invalid!")
|
||||||
|
|
||||||
|
raw_tx_parts = [
|
||||||
|
"0000000a", # type 10 ARBITRARY
|
||||||
|
f"{int(time.time() * 1000):016x}", # current timestamp
|
||||||
|
"00000001", # dev group ID
|
||||||
|
reference_hex, # reference
|
||||||
|
pubkey_hex, # pubkey
|
||||||
|
"00000000", # nonce
|
||||||
|
"00000000", # name length
|
||||||
|
"00000000", # identifier length
|
||||||
|
"00000000", # method (PUT)
|
||||||
|
"00000000", # secret length
|
||||||
|
"00000000", # compression
|
||||||
|
"00000000", # number of payments
|
||||||
|
"00000001", # service ID
|
||||||
|
"01", # data type (RAW_DATA)
|
||||||
|
f"{int(len(data_hex)//2):08x}", # data length
|
||||||
|
data_hex, # payload
|
||||||
|
f"{int(len(data_hex)//2):08x}", # repeated data length
|
||||||
|
"00000000", # metadata hash length
|
||||||
|
f"{int(0.01 * 1e8):016x}" # fee
|
||||||
|
]
|
||||||
|
tx_hex = "".join(raw_tx_parts)
|
||||||
|
|
||||||
|
|
||||||
|
tx_base58 = to_base58(tx_hex, args.port)
|
||||||
|
signed_tx = sign_transaction(privkey, tx_base58, args.port)
|
||||||
|
|
||||||
|
print("Submitting in 5 seconds... press CTRL+C to cancel")
|
||||||
|
for i in range(5, 0, -1):
|
||||||
|
print(f"{i}...", end='\r', flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if not process_transaction(signed_tx, args.port):
|
||||||
|
sys.exit("Transaction submission failed")
|
||||||
|
|
||||||
|
decoded = decode_transaction(signed_tx, args.port)
|
||||||
|
print("\nTransaction submitted successfully:")
|
||||||
|
print(decoded)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user