From 1a67fab14ae8b17d6176c472841b2fa8329166b4 Mon Sep 17 00:00:00 2001 From: crowetic Date: Wed, 9 Apr 2025 17:48:02 -0700 Subject: [PATCH] Initial Commit - including auto-update automation scripts from build to publish --- README.md | 130 +++++++++ backups/README.md | 76 +++++ ...pdate.sh-(Tue Apr 8 02:57:31 PM PDT 2025) | 148 ++++++++++ ...pdate.sh-(Tue Apr 8 08:33:23 PM PDT 2025) | 182 ++++++++++++ build-auto-update.sh | 264 ++++++++++++++++++ original/publish-auto-update.pl | 206 ++++++++++++++ publish-auto-update.py | 234 ++++++++++++++++ 7 files changed, 1240 insertions(+) create mode 100644 README.md create mode 100644 backups/README.md create mode 100755 backups/build-auto-update.sh-(Tue Apr 8 02:57:31 PM PDT 2025) create mode 100755 backups/build-auto-update.sh-(Tue Apr 8 08:33:23 PM PDT 2025) create mode 100755 build-auto-update.sh create mode 100644 original/publish-auto-update.pl create mode 100755 publish-auto-update.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6b9a97 --- /dev/null +++ b/README.md @@ -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-` 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 +``` + +> πŸ” Private key is always prompted securely unless passed explicitly (e.g. from automation). + +This script will: +- Detect the latest `auto-update-` 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-` +- 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'. + diff --git a/backups/README.md b/backups/README.md new file mode 100644 index 0000000..bbdbd81 --- /dev/null +++ b/backups/README.md @@ -0,0 +1,76 @@ +# Qortal Auto-Update Publisher Script + +This script provides a modern, simplified, and testable way to publish an auto-update transaction for Qortal. It replaces the legacy Perl-based script with a more maintainable Python version. + +## πŸ”§ Requirements (Before Running) + +Ensure the following steps and conditions are met: + +1. **Node Environment** + - A local Qortal node must be running and fully synced. + - The node must expose its API (default port: `12391`). + - Ensure your node's `settings.json` includes access to the relevant endpoints (i.e., it's not locked down). + +2. **Qortal Update Prepared** + - You have run the `tools/build-auto-update.sh` script (or the improved `build-auto-update.sh` version). + - This should generate a `.update` file (e.g. `qortal.update`) and commit it to a new branch named: `auto-update-`. + +3. **Git Repository Setup** + - You must be inside the root of the Qortal Git repository. + - The `pom.xml` file should contain the correct ``. + - You have pushed your commit + branch to a public GitHub repository, preferably a fork (for testing). + +4. **Authentication** + - You possess the **Base58-encoded private key** for a non-admin member of the `dev` group. + - The key must correspond to a Qortal account that can submit `ARBITRARY` transactions to the group (group ID 1). + +5. **Python Requirements** + - Python 3.6+ + - `requests` package (`pip install requests`) + +## πŸš€ Full Auto-Update Workflow + +### Step 1: Prepare Your Code +- Ensure your latest code changes are committed and pushed. +- Bump the version in `pom.xml` if needed. +- Tag the commit with the version number (e.g. `v1.4.2`) and push the tag. + +```bash +git commit -m "Bump version to 1.4.2" pom.xml +git tag v1.4.2 +git push origin v1.4.2 +``` + +### Step 2: Build the XOR-Obfuscated Update +Use the improved `build-auto-update.sh`: +```bash +./build-auto-update.sh +``` +- This builds the JAR. +- XORs it into `qortal.update`. +- Creates a new branch named `auto-update-` and pushes it. + +### Step 3: Test the Update Transaction (Dry Run) +```bash +python3 publish_auto_update.py --repo YourUser/qortal-test --dry-run +``` +- This will validate the timestamp, hash, and download URL, without submitting anything to the chain. + +### Step 4: Publish the Auto-Update +```bash +python3 publish_auto_update.py --repo YourUser/qortal-test +``` +- This will sign and submit the auto-update transaction. + +### Step 5: Approve the Update (Admin Only) +- Use `tools/approve-auto-update.sh` from a dev group admin account to approve the update. +- A minimum number of approvals + block confirmations will trigger update rollout. + +### Step 6: Monitor and Confirm +- Nodes will download the update over the next ~20 minutes (per `CHECK_INTERVAL`). +- Confirm via logs or block explorer that the update was received and applied. + +--- + +By following this workflow, new contributors and developers can easily participate in the Qortal auto-update process with minimal risk and clear validation checkpoints. + diff --git a/backups/build-auto-update.sh-(Tue Apr 8 02:57:31 PM PDT 2025) b/backups/build-auto-update.sh-(Tue Apr 8 02:57:31 PM PDT 2025) new file mode 100755 index 0000000..38b1758 --- /dev/null +++ b/backups/build-auto-update.sh-(Tue Apr 8 02:57:31 PM PDT 2025) @@ -0,0 +1,148 @@ +#!/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 + +# === 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 $*" + $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-mvn-build-$(date +%Y%m%d-%H%M%S).log" +echo "Logging to: ${LOG_FILE}" + +# === 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 + +# === 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}" + +# === 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 + +# === Extract Info === +short_hash=$(git rev-parse --short HEAD) +[[ -z "${short_hash}" ]] && abort "Unable to extract commit hash." +echo "Using commit hash: ${short_hash}" + +project=$(grep -oPm1 "(?<=)[^<]+" 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 "(?<=)[^<]+" pom.xml) +new_version=$(increment_version "$current_version") + +$DRY_RUN || sed -i "s|${current_version}|${new_version}|" pom.xml + +echo "Updated version from ${current_version} to ${new_version} in pom.xml" +git diff pom.xml +confirm_or_exit "Is the updated version correct?" + +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}" + +# === Build JAR === +echo "Building JAR for ${project}..." +if ! $DRY_RUN; then + mvn clean package &> "${LOG_FILE}" || { + tail -n 20 "${LOG_FILE}" + abort "Maven build failed. See full log: ${LOG_FILE}" + } +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}." + diff --git a/backups/build-auto-update.sh-(Tue Apr 8 08:33:23 PM PDT 2025) b/backups/build-auto-update.sh-(Tue Apr 8 08:33:23 PM PDT 2025) new file mode 100755 index 0000000..fb43a3a --- /dev/null +++ b/backups/build-auto-update.sh-(Tue Apr 8 08:33:23 PM PDT 2025) @@ -0,0 +1,182 @@ +#!/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 $*" + $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-mvn-build-$(date +%Y%m%d-%H%M%S).log" +echo "Logging to: ${LOG_FILE}" + +# === 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}" + +# === 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 + +# === Extract Info === +short_hash=$(git rev-parse --short HEAD) +[[ -z "${short_hash}" ]] && abort "Unable to extract commit hash." +echo "Using commit hash: ${short_hash}" + +project=$(grep -oPm1 "(?<=)[^<]+" 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 "(?<=)[^<]+" pom.xml) +new_version=$(increment_version "$current_version") + +$DRY_RUN || sed -i "s|${current_version}|${new_version}|" pom.xml + +echo "Updated version from ${current_version} to ${new_version} in pom.xml" +git diff pom.xml +confirm_or_exit "Is the updated version correct?" + +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}" + +# === Build JAR === +echo "Building JAR for ${project}..." +if ! $DRY_RUN; then + mvn clean package &> "${LOG_FILE}" || { + tail -n 20 "${LOG_FILE}" + abort "Maven build failed. See full log: ${LOG_FILE}" + } +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}." + +# === 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 + 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 + + diff --git a/build-auto-update.sh b/build-auto-update.sh new file mode 100755 index 0000000..5113823 --- /dev/null +++ b/build-auto-update.sh @@ -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 "(?<=)[^<]+" 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 "(?<=)[^<]+" pom.xml) +new_version=$(increment_version "$current_version") + +$DRY_RUN || sed -i "s|${current_version}|${new_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|${new_version}|${user_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 + + diff --git a/original/publish-auto-update.pl b/original/publish-auto-update.pl new file mode 100644 index 0000000..ed473ac --- /dev/null +++ b/original/publish-auto-update.pl @@ -0,0 +1,206 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use POSIX; +use Getopt::Std; +use File::Slurp; + +sub usage() { + die("usage: $0 [-p api-port] dev-private-key [short-commit-hash]\n"); +} + +my %opt; +getopts('p:', \%opt); + +usage() if @ARGV < 1 || @ARGV > 2; + +my $port = $opt{p} || 12391; +my $privkey = shift @ARGV; +my $commit_hash = shift @ARGV; + +my $git_dir = `git rev-parse --show-toplevel`; +die("Cannot determine git top level dir\n") unless $git_dir; + +chomp $git_dir; +chdir($git_dir) || die("Can't change directory to $git_dir: $!\n"); + +open(POM, '<', 'pom.xml') || die ("Can't open 'pom.xml': $!\n"); +my $project; +while () { + if (m/(\w+)<.artifactId>/o) { + $project = $1; + last; + } +} +close(POM); + +my $apikey = read_file('apikey.txt'); + +# Do we need to determine commit hash? +unless ($commit_hash) { + # determine git branch + my $branch_name = ` git symbolic-ref -q HEAD `; + chomp $branch_name; + $branch_name =~ s|^refs/heads/||; # ${branch_name##refs/heads/} + + # short-form commit hash on base branch (non-auto-update) + $commit_hash ||= `git show --no-patch --format=%h`; + die("Can't find commit hash\n") if ! defined $commit_hash; + chomp $commit_hash; + printf "Commit hash on '%s' branch: %s\n", $branch_name, $commit_hash; +} else { + printf "Using given commit hash: %s\n", $commit_hash; +} + +# build timestamp / commit timestamp on base branch +my $timestamp = `git show --no-patch --format=%ct ${commit_hash}`; +die("Can't determine commit timestamp\n") if ! defined $timestamp; +$timestamp *= 1000; # Convert to milliseconds + +# locate sha256 utility +my $SHA256 = `which sha256sum || which sha256`; +chomp $SHA256; +die("Can't find sha256sum or sha256\n") unless length($SHA256) > 0; + +# SHA256 of actual update file +my $sha256 = `git show auto-update-${commit_hash}:${project}.update | ${SHA256} | head -c 64`; +die("Can't calculate SHA256 of ${project}.update\n") unless $sha256 =~ m/(\S{64})/; +chomp $sha256; + +# long-form commit hash of HEAD on auto-update branch +#my $update_hash = `git rev-parse refs/heads/auto-update-${commit_hash}`; +my $update_hash = `git rev-parse origin/auto-update-${commit_hash}`; +die("Can't find commit hash for HEAD on auto-update-${commit_hash} branch\n") if ! defined $update_hash; +chomp $update_hash; + +printf "Build timestamp (ms): %d / 0x%016x\n", $timestamp, $timestamp; +printf "Auto-update commit hash: %s\n", $update_hash; +printf "SHA256 of ${project}.update: %s\n", $sha256; + +my $tx_type = 10; +my $tx_timestamp = time() * 1000; +my $tx_group_id = 1; +my $service = 1; +printf "\nARBITRARY(%d) transaction with timestamp %d, txGroupID %d and service %d\n", $tx_type, $tx_timestamp, $tx_group_id, $service; + +my $data_hex = sprintf "%016x%s%s", $timestamp, $update_hash, $sha256; +printf "\nARBITRARY transaction data payload: %s\n", $data_hex; + +my $n_payments = 0; +my $data_type = 1; # RAW_DATA +my $data_length = length($data_hex) / 2; # two hex chars per byte +my $fee = 0.01 * 1e8; +my $nonce = 0; +my $name_length = 0; +my $identifier_length = 0; +my $method = 0; # PUT +my $secret_length = 0; +my $compression = 0; # None +my $metadata_hash_length = 0; + +die("Something's wrong: data length is not 60 bytes!\n") if $data_length != 60; + +my $pubkey = `curl --silent --url http://localhost:${port}/utils/publickey --data ${privkey}`; +die("Can't convert private key to public key:\n$pubkey\n") unless $pubkey =~ m/^\w{44}$/; +printf "\nPublic key: %s\n", $pubkey; + +my $pubkey_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${pubkey}`; +die("Can't convert base58 public key to hex:\n$pubkey_hex\n") unless $pubkey_hex =~ m/^[A-Za-z0-9]{64}$/; +printf "Public key hex: %s\n", $pubkey_hex; + +my $address = `curl --silent --url http://localhost:${port}/addresses/convert/${pubkey}`; +die("Can't convert base58 public key to address:\n$address\n") unless $address =~ m/^\w{33,34}$/; +printf "Address: %s\n", $address; + +my $reference = `curl --silent --url http://localhost:${port}/addresses/lastreference/${address}`; +die("Can't fetch last reference for $address:\n$reference\n") unless $reference =~ m/^\w{87,88}$/; +printf "Last reference: %s\n", $reference; + +my $reference_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${reference}`; +die("Can't convert base58 reference to hex:\n$reference_hex\n") unless $reference_hex =~ m/^[A-Za-z0-9]{128}$/; +printf "Last reference hex: %s\n", $reference_hex; + +my $raw_tx_hex = sprintf("%08x%016x%08x%s%s%08x%08x%08x%08x%08x%08x%08x%08x%02x%08x%s%08x%08x%016x", $tx_type, $tx_timestamp, $tx_group_id, $reference_hex, $pubkey_hex, $nonce, $name_length, $identifier_length, $method, $secret_length, $compression, $n_payments, $service, $data_type, $data_length, $data_hex, $data_length, $metadata_hash_length, $fee); +printf "\nRaw transaction hex:\n%s\n", $raw_tx_hex; + +my $raw_tx = `curl --silent --url http://localhost:${port}/utils/tobase58/${raw_tx_hex}`; +die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{300,320}$/; # Roughly 305 to 320 base58 chars +printf "\nRaw transaction (base58):\n%s\n", $raw_tx; + +my $sign_data = qq|' { "privateKey": "${privkey}", "transactionBytes": "${raw_tx}" } '|; +my $signed_tx = `curl --silent -H "accept: text/plain" -H "Content-Type: application/json" --url http://localhost:${port}/transactions/sign --data ${sign_data}`; +die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{390,410}$/; # +90ish longer than $raw_tx +printf "\nSigned transaction:\n%s\n", $signed_tx; + +# Get the origin URL - So that we will be able to TEST the obtaining of the qortal.update... +my $origin = `git remote get-url origin`; +chomp $origin; # Remove any trailing newlines +die("Unable to get github url for 'origin'?\n") unless $origin; + +# Debug: Print the origin URL +print "Full Origin URL: $origin\n"; + +# Extract the repository path (e.g., Qortal/qortal) NOTE - github is case-sensitive with repo names +my $repo; +if ($origin =~ m/[:\/]([\w\-]+\/[\w\-]+)\.git$/) { + $repo = $1; + print "Extracted direct repository path: $repo\n"; + if ($repo =~ m/^qortal\//i) { + $repo =~ s/^qortal\//Qortal\//; + print "Corrected repository path capitalization: $repo\n"; + } + print "Please verify the direct repository path. Current: '$repo'\n"; + print "If incorrect, input the correct direct repository path (e.g., 'Qortal/qortal' or 'bob/qortal').NOTE - github is CASE SENSITIVE for repository urls... Press Enter to keep the extracted version: "; + my $input = ; + if ($input =~ m/^qortal\//i) { + $input =~ s/^qortal\//Qortal\//; + print "Corrected repository path capitalization: $repo\n"; + } + chomp $input; + $repo = $input if $input; # Update repo if user provides input + +} else { + # Default to qortal/qortal if extraction fails + $repo = "Qortal/qortal"; + print "Failed to extract repository path from origin URL. Using default: $repo\n"; + + # Prompt the user for confirmation or input + print "Please verify the repository path. Current: '$repo'\n"; + print "If incorrect, input the correct repository path (e.g., 'Qortal/qortal' or 'BobsCodeburgers/qortal'). NOTE - GitHub is CASE SENSITIVE for repository urls... Press Enter to keep the default: "; + my $input = ; + if ($input =~ m/^qortal\//i) { + $input =~ s/^qortal\//Qortal\//; + print "Corrected repository path capitalization: $repo\n"; + } + chomp $input; + $repo = $input if $input; # Update repo if user provides input +} + +# Debug: Print the final repository path +print "Final direct repository path: $repo\n"; + +# Construct the update URL +my $update_url = "https://github.com/${repo}/raw/${update_hash}/${project}.update"; +print "Final update URL: $update_url\n"; + + +my $fetch_result = `curl --silent -o /dev/null --location --range 0-1 --head --write-out '%{http_code}' --url ${update_url}`; +die("\nUnable to fetch update from ${update_url}\n") if $fetch_result ne '200'; +printf "\nUpdate fetchable from ${update_url}\n"; + +# Flush STDOUT after every output +$| = 1; +print "\n"; +for (my $delay = 5; $delay > 0; --$delay) { + printf "\rSubmitting transaction in %d second%s... CTRL-C to abort ", $delay, ($delay != 1 ? 's' : ''); + sleep 1; +} + +printf "\rSubmitting transaction NOW... \n"; +my $result = `curl --silent --url http://localhost:${port}/transactions/process --data ${signed_tx}`; +chomp $result; +die("Transaction wasn't accepted:\n$result\n") unless $result eq 'true'; + +my $decoded_tx = `curl --silent -H "Content-Type: application/json" --url http://localhost:${port}/transactions/decode --data ${signed_tx}`; +printf "\nTransaction accepted:\n$decoded_tx\n"; diff --git a/publish-auto-update.py b/publish-auto-update.py new file mode 100755 index 0000000..4225fc8 --- /dev/null +++ b/publish-auto-update.py @@ -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 '' 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() +