Initial Commit - including auto-update automation scripts from build to publish

This commit is contained in:
crowetic 2025-04-09 17:48:02 -07:00
commit 1a67fab14a
7 changed files with 1240 additions and 0 deletions

130
README.md Normal file
View 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 Qortals 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'.

76
backups/README.md Normal file
View File

@ -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-<commit-hash>`.
3. **Git Repository Setup**
- You must be inside the root of the Qortal Git repository.
- The `pom.xml` file should contain the correct `<artifactId>`.
- 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-<commit>` and pushes it.
### Step 3: Test the Update Transaction (Dry Run)
```bash
python3 publish_auto_update.py <Base58PrivateKey> <CommitHash> --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 <Base58PrivateKey> <CommitHash> --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.

View File

@ -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 "(?<=<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
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}."

View File

@ -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 "(?<=<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
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

264
build-auto-update.sh Executable file
View 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

View File

@ -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 (<POM>) {
if (m/<artifactId>(\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 = <STDIN>;
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 = <STDIN>;
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";

234
publish-auto-update.py Executable file
View 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()