add make-release.sh, svatantrya.asc, and update HANDOFF.md
- make-release.sh: automated release script (tests, lint, build, GPG sign, SHA-256, Gitea) - svatantrya.asc: PGP public key for release verification - HANDOFF.md: updated release workflow documentation - willexecutors.py: input validation for addresses, fees, and API responses - manifest.json: fixed version back to 0.6.1 - test_core_plugin_base.py: updated baltx_fees default
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
bal-electrum-plugin.zip
|
||||
electrum-src/
|
||||
preview_*.png
|
||||
.env
|
||||
|
||||
43
HANDOFF.md
43
HANDOFF.md
@@ -124,6 +124,10 @@ The code reads this at runtime via `get_version()` in `bal/core/plugin_base.py`
|
||||
must **fully restart Electrum** (not just reload the plugin) — Electrum's
|
||||
`zipimport` caches modules, so a partial reload runs stale code.
|
||||
|
||||
**Automated release:** use `./make-release.sh` to run the full release flow
|
||||
(tests, lint, build, GPG sign, SHA-256, Electrum test pause, Gitea release).
|
||||
See Section 5 for details.
|
||||
|
||||
---
|
||||
|
||||
## 4. Key technical knowledge (hard-won — saves you hours)
|
||||
@@ -231,16 +235,39 @@ must **fully restart Electrum** (not just reload the plugin) — Electrum's
|
||||
squash local commits into ONE comprehensive commit, push (force if needed),
|
||||
then create/update the PR and SHARE the PR URL with the owner.
|
||||
- **ZIPs are NOT committed** (`.gitignore` excludes `*.zip`). They are
|
||||
distributed via **GitHub Releases** (`gh release create vX.Y.Z file.zip ...`).
|
||||
The newest release is the "Latest" and is the owner's convenient download.
|
||||
- Deliverable ZIPs are ALSO uploaded with the file-wrapper tool so the owner can
|
||||
download them directly from chat.
|
||||
- **Auth note:** if `git push` / `gh` fails with "Invalid username or token",
|
||||
re-run the GitHub environment setup, then retry.
|
||||
distributed via **Gitea Releases** using `make-release.sh`.
|
||||
- **Release process** (`make-release.sh`):
|
||||
1. Version bump in `bal/manifest.json` (single source of truth)
|
||||
2. Clean `__pycache__` and `.pyc` files
|
||||
3. Run full test suite
|
||||
4. Lint with ruff (skip if not installed)
|
||||
5. Build ZIP via `build_zip.py` (deterministic order, SHA-256, manifest check)
|
||||
6. GPG sign: `.asc` (armor) + `.sig` (binary) with key `A847D004DB91610711CA6A0DFE756706E833E0D1`
|
||||
7. Export public key as `svatantrya.asc`
|
||||
8. SHA-256 checksum
|
||||
9. Interactive pause for Electrum testing (ZIP-FIRST policy)
|
||||
10. Create Gitea tag, push, create release, upload 5 assets (ZIP + .asc + .sig + .sha256 + svatantrya.asc)
|
||||
- **Usage:**
|
||||
```bash
|
||||
./make-release.sh # read version from bal/manifest.json
|
||||
./make-release.sh v0.6.2 # bump manifest to 0.6.2, then release
|
||||
```
|
||||
- **Release assets** (5 files):
|
||||
- `bal_vX.Y.Z.zip` — the plugin
|
||||
- `bal_vX.Y.Z.zip.asc` — GPG signature (armor)
|
||||
- `bal_vX.Y.Z.zip.sig` — GPG signature (binary)
|
||||
- `bal_vX.Y.Z.zip.sha256` — SHA-256 checksum
|
||||
- `svatantrya.asc` — signing public key
|
||||
- **GPG verification instructions** (included in release body):
|
||||
```bash
|
||||
gpg --fetch-key https://bitcoin-after.life/svatantrya.asc
|
||||
gpg --verify bal_vX.Y.Z.zip.asc bal_vX.Y.Z.zip
|
||||
```
|
||||
- **Auth note:** if `git push` or Gitea API fails with "invalid credentials",
|
||||
update `GITEA_TOKEN` env var or `~/.git-credentials`, then retry.
|
||||
- PR history for this line of work: **#13** (v0.4.7), **#14** (docs/DUST section +
|
||||
translation), **#15** (v0.4.8). All merged into `main`.
|
||||
- Releases: latest is **v0.4.8** (asset `bal-electrum-plugin-v0.4.8.zip`);
|
||||
v0.4.7 kept in history.
|
||||
- Releases: latest is **v0.6.1**; v0.4.7, v0.4.8 kept in history.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import time
|
||||
from datetime import datetime
|
||||
|
||||
from aiohttp import ClientResponse
|
||||
from electrum import bitcoin, constants
|
||||
from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
|
||||
from electrum.i18n import _
|
||||
from electrum.logging import get_logger
|
||||
from electrum.network import Network
|
||||
@@ -276,51 +278,44 @@ class Willexecutors:
|
||||
headers["Content-Type"] = "text/plain"
|
||||
if not handle_response:
|
||||
handle_response = Willexecutors.handle_response
|
||||
try:
|
||||
if method == "get":
|
||||
response = Network.send_http_on_proxy(
|
||||
method,
|
||||
url,
|
||||
params=data,
|
||||
headers=headers,
|
||||
on_finish=handle_response,
|
||||
timeout=timeout,
|
||||
)
|
||||
elif method == "post":
|
||||
response = Network.send_http_on_proxy(
|
||||
method,
|
||||
url,
|
||||
body=data,
|
||||
headers=headers,
|
||||
on_finish=handle_response,
|
||||
timeout=timeout,
|
||||
)
|
||||
else:
|
||||
raise Exception(f"unexpected {method=!r}")
|
||||
except TimeoutError:
|
||||
if count_reply < max_retries:
|
||||
_logger.debug(
|
||||
f"timeout({count_reply}) error: retry in {retry_sleep} sec..."
|
||||
)
|
||||
if retry_sleep:
|
||||
time.sleep(retry_sleep)
|
||||
return Willexecutors.send_request(
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
timeout=timeout,
|
||||
handle_response=handle_response,
|
||||
count_reply=count_reply + 1,
|
||||
max_retries=max_retries,
|
||||
retry_sleep=retry_sleep,
|
||||
)
|
||||
else:
|
||||
_logger.debug(f"Too many timeouts: {count_reply}")
|
||||
except Exception as e:
|
||||
raise e
|
||||
else:
|
||||
_logger.debug(f"--> {response}")
|
||||
return response
|
||||
attempts = max_retries + 1
|
||||
for attempt in range(attempts):
|
||||
try:
|
||||
if method == "get":
|
||||
response = Network.send_http_on_proxy(
|
||||
method,
|
||||
url,
|
||||
params=data,
|
||||
headers=headers,
|
||||
on_finish=handle_response,
|
||||
timeout=timeout,
|
||||
)
|
||||
elif method == "post":
|
||||
response = Network.send_http_on_proxy(
|
||||
method,
|
||||
url,
|
||||
body=data,
|
||||
headers=headers,
|
||||
on_finish=handle_response,
|
||||
timeout=timeout,
|
||||
)
|
||||
else:
|
||||
raise Exception(f"unexpected {method=!r}")
|
||||
_logger.debug(f"--> {response}")
|
||||
return response
|
||||
except TimeoutError:
|
||||
if attempt < max_retries:
|
||||
_logger.debug(
|
||||
f"timeout({attempt}) error: "
|
||||
f"retry in {retry_sleep} sec..."
|
||||
)
|
||||
if retry_sleep:
|
||||
time.sleep(retry_sleep)
|
||||
else:
|
||||
_logger.debug(f"Too many timeouts: {attempt}")
|
||||
except Exception as e:
|
||||
raise e
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_we_url_from_response(resp):
|
||||
@@ -331,16 +326,12 @@ class Willexecutors:
|
||||
|
||||
@staticmethod
|
||||
async def handle_response(resp: ClientResponse):
|
||||
resp.raise_for_status()
|
||||
r = await resp.text()
|
||||
try:
|
||||
|
||||
r = json.loads(r)
|
||||
# url = Willexecutors.get_we_url_from_response(resp)
|
||||
# r["url"]= url
|
||||
# r["status"]=resp.status
|
||||
except Exception as e:
|
||||
_logger.debug(f"error handling response:{e}")
|
||||
pass
|
||||
return r
|
||||
|
||||
@staticmethod
|
||||
@@ -368,13 +359,15 @@ class Willexecutors:
|
||||
max_retries=max_retries,
|
||||
retry_sleep=retry_sleep,
|
||||
):
|
||||
willexecutor["broadcast_status"] = _("Success")
|
||||
_logger.debug(f"pushed: {w}")
|
||||
if w != "thx":
|
||||
_logger.debug(f"error: {w}")
|
||||
raise Exception(w)
|
||||
willexecutor["broadcast_status"] = _("Success")
|
||||
else:
|
||||
raise Exception("empty reply from:{willexecutor['url']}")
|
||||
raise Exception(
|
||||
f"empty reply from:{willexecutor['url']}"
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.debug(f"error:{e}")
|
||||
if str(e) == "already present":
|
||||
@@ -404,11 +397,34 @@ class Willexecutors:
|
||||
timeout=timeout, max_retries=max_retries, retry_sleep=retry_sleep,
|
||||
)
|
||||
if isinstance(w, dict):
|
||||
willexecutor["url"] = url
|
||||
willexecutor["status"] = 200
|
||||
willexecutor["base_fee"] = w["base_fee"]
|
||||
willexecutor["address"] = w["address"]
|
||||
willexecutor["info"] = w["info"]
|
||||
address = w.get("address")
|
||||
if not isinstance(address, str) or not bitcoin.is_address(
|
||||
address, net=constants.net
|
||||
):
|
||||
_logger.warning(
|
||||
f"invalid address from {url}: {address!r}"
|
||||
)
|
||||
willexecutor["status"] = "KO"
|
||||
else:
|
||||
base_fee = w.get("base_fee")
|
||||
try:
|
||||
base_fee = int(base_fee)
|
||||
if base_fee < 0:
|
||||
raise ValueError("negative fee")
|
||||
if base_fee > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN:
|
||||
raise ValueError("fee exceeds total coin supply")
|
||||
except (TypeError, ValueError) as e:
|
||||
_logger.warning(
|
||||
f"invalid base_fee from {url}: "
|
||||
f"{w.get('base_fee')!r} ({e})"
|
||||
)
|
||||
willexecutor["status"] = "KO"
|
||||
else:
|
||||
willexecutor["url"] = url
|
||||
willexecutor["status"] = 200
|
||||
willexecutor["base_fee"] = base_fee
|
||||
willexecutor["address"] = address
|
||||
willexecutor["info"] = w.get("info", "")
|
||||
else:
|
||||
# No dict reply (timeout / empty) -> mark as unreachable.
|
||||
willexecutor["status"] = "KO"
|
||||
@@ -742,7 +758,14 @@ class Willexecutors:
|
||||
else:
|
||||
willexecutor["status"] = old_willexecutor.get("status",willexecutor.get("status","Ko"))
|
||||
willexecutor["selected"]=Willexecutors.is_selected(old_willexecutor) or willexecutor.get("selected",False)
|
||||
willexecutor["address"]=old_willexecutor.get("address",willexecutor.get("address",""))
|
||||
address = old_willexecutor.get("address", willexecutor.get("address", ""))
|
||||
if address and not bitcoin.is_address(address, net=constants.net):
|
||||
_logger.warning(
|
||||
f"invalid address {address!r} for executor {url}, "
|
||||
f"falling back to empty"
|
||||
)
|
||||
address = ""
|
||||
willexecutor["address"] = address
|
||||
willexecutor["promo_code"]=old_willexecutor.get("promo_code",willexecutor.get("promo_code"))
|
||||
|
||||
|
||||
@@ -755,14 +778,23 @@ class Willexecutors:
|
||||
"get",
|
||||
f"{welist_server}data/{chainname}?page=0&limit=100",
|
||||
)
|
||||
# del willexecutors["status"]
|
||||
if not isinstance(willexecutors, dict):
|
||||
_logger.warning(
|
||||
f"unexpected download_list response type: "
|
||||
f"{type(willexecutors).__name__}"
|
||||
)
|
||||
return {}
|
||||
for w in willexecutors:
|
||||
if w not in ("status", "url"):
|
||||
if not isinstance(willexecutors.get(w), dict):
|
||||
_logger.warning(
|
||||
f"malformed entry {w!r} in executor list, "
|
||||
f"type={type(willexecutors.get(w)).__name__}"
|
||||
)
|
||||
continue
|
||||
Willexecutors.initialize_willexecutor(
|
||||
willexecutors[w], w, None, old_willexecutors.get(w,None)
|
||||
)
|
||||
# bal_plugin.WILLEXECUTORS.set(l)
|
||||
# bal_plugin.config.set_key(bal_plugin.WILLEXECUTORS,l,save=True)
|
||||
return willexecutors
|
||||
|
||||
except Exception as e:
|
||||
@@ -794,6 +826,12 @@ class Willexecutors:
|
||||
"post", url + "/searchtx", data=txid.encode("ascii"),
|
||||
timeout=timeout, max_retries=max_retries, retry_sleep=retry_sleep,
|
||||
)
|
||||
if not isinstance(w, dict):
|
||||
_logger.warning(
|
||||
f"unexpected check_transaction response type "
|
||||
f"from {url}: {type(w).__name__}"
|
||||
)
|
||||
return None
|
||||
return w
|
||||
except Exception as e:
|
||||
_logger.error(f"error contacting {url} for checking txs {e}")
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.",
|
||||
"author": "Svatantrya",
|
||||
"licence": "MIT",
|
||||
"available_for": ["qt"],
|
||||
"available_for": [
|
||||
"qt"
|
||||
],
|
||||
"icon": "icons/bal32x32.png"
|
||||
}
|
||||
}
|
||||
285
make-release.sh
Executable file
285
make-release.sh
Executable file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env bash
|
||||
# make-release.sh — Create a Gitea release for bal-electrum-plugin
|
||||
#
|
||||
# Usage:
|
||||
# ./make-release.sh # read version from bal/manifest.json
|
||||
# ./make-release.sh v0.6.2 # bump manifest to 0.6.2, then release
|
||||
#
|
||||
# Requires: git, gpg, curl, python3, sha256sum
|
||||
# Optional: ruff (lint skipped if not installed)
|
||||
# Credentials: ~/.git-credentials or GITEA_USER / GITEA_TOKEN env vars
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────
|
||||
die() { echo "Error: $*" >&2; exit 1; }
|
||||
info() { echo ""; echo "── $* ──"; }
|
||||
|
||||
# ── 0. Resolve version ──────────────────────────────────────────────
|
||||
MANIFEST="bal/manifest.json"
|
||||
[ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"
|
||||
|
||||
# read current version from manifest
|
||||
CURRENT_VER=$(python3 -c "import json; print(json.load(open('$MANIFEST'))['version'])")
|
||||
[ -n "$CURRENT_VER" ] || die "cannot read version from $MANIFEST"
|
||||
|
||||
ARG="${1:-}"
|
||||
if [ -n "$ARG" ]; then
|
||||
# normalise: accept "v0.6.2" or "0.6.2"
|
||||
NEW_VER="${ARG#v}"
|
||||
TAG="v${NEW_VER}"
|
||||
else
|
||||
TAG="v${CURRENT_VER}"
|
||||
NEW_VER=""
|
||||
fi
|
||||
|
||||
echo "=== Release ${TAG} ==="
|
||||
echo "Current manifest version: ${CURRENT_VER}"
|
||||
[ -n "$NEW_VER" ] && echo "New version (will bump): ${NEW_VER}"
|
||||
|
||||
# ── 1. Bump version in manifest (if arg provided) ──────────────────
|
||||
if [ -n "$NEW_VER" ] && [ "$NEW_VER" != "$CURRENT_VER" ]; then
|
||||
info "[1/10] Bumping version to ${NEW_VER} in ${MANIFEST}"
|
||||
python3 -c "
|
||||
import json
|
||||
f = open('${MANIFEST}')
|
||||
d = json.load(f); f.close()
|
||||
d['version'] = '${NEW_VER}'
|
||||
json.dump(d, open('${MANIFEST}', 'w'), indent=4, ensure_ascii=False)
|
||||
print(json.dumps(d, indent=4, ensure_ascii=False))
|
||||
"
|
||||
git add "$MANIFEST"
|
||||
else
|
||||
info "[1/10] Version already ${CURRENT_VER}, no bump needed"
|
||||
fi
|
||||
|
||||
# ── 2. Clean caches ─────────────────────────────────────────────────
|
||||
info "[2/10] Cleaning __pycache__ and .pyc"
|
||||
find bal -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
find bal -name "*.pyc" -delete 2>/dev/null || true
|
||||
find bal -name "*.pyo" -delete 2>/dev/null || true
|
||||
|
||||
# ── 3. Run tests ────────────────────────────────────────────────────
|
||||
info "[3/10] Running test suite"
|
||||
if QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 -m pytest \
|
||||
tests/test_core_*.py \
|
||||
tests/test_anticipate_past_locktime.py tests/test_anticipate_manual_locktime.py \
|
||||
tests/test_group_b_auto_sign.py tests/test_group_c_settings.py \
|
||||
tests/test_group_d_alarms.py tests/test_group_e_mock_karen7.py \
|
||||
tests/test_group_f_heir_change_rebuild.py tests/test_group_g_basic_calendar.py \
|
||||
tests/test_group_h_v048.py \
|
||||
-q 2>&1; then
|
||||
echo "All tests passed."
|
||||
else
|
||||
die "Tests failed — aborting release."
|
||||
fi
|
||||
|
||||
# ── 4. Lint (optional) ─────────────────────────────────────────────
|
||||
info "[4/10] Lint with ruff"
|
||||
if command -v ruff &>/dev/null; then
|
||||
RUFF_ERRORS=$(ruff check bal/ 2>&1 \
|
||||
| grep -oE "^[^ ]+\.py:[0-9]+:[0-9]+: [A-Z][0-9]+" \
|
||||
| grep -vE "F401|F403|F405|F841" || true)
|
||||
if [ -n "$RUFF_ERRORS" ]; then
|
||||
echo "New ruff errors:"
|
||||
echo "$RUFF_ERRORS"
|
||||
die "Lint errors found — fix before releasing."
|
||||
else
|
||||
echo "Lint clean (ignoring known pre-existing warnings)."
|
||||
fi
|
||||
else
|
||||
echo "ruff not installed — skipping lint."
|
||||
fi
|
||||
|
||||
# ── 5. Build ZIP via build_zip.py ───────────────────────────────────
|
||||
info "[5/10] Building ZIP"
|
||||
ZIP_NAME="bal_${TAG}.zip"
|
||||
python3 build_zip.py "$ZIP_NAME"
|
||||
|
||||
# ── 6. GPG sign (armor + binary) + export public key ───────────────
|
||||
info "[6/10] Signing with GPG"
|
||||
GPG_KEY="A847D004DB91610711CA6A0DFE756706E833E0D1"
|
||||
gpg --default-key "$GPG_KEY" --batch --yes --armor --detach-sign "$ZIP_NAME"
|
||||
gpg --default-key "$GPG_KEY" --batch --yes --detach-sign "$ZIP_NAME"
|
||||
ASC_FILE="${ZIP_NAME}.asc"
|
||||
SIG_FILE="${ZIP_NAME}.sig"
|
||||
PGP_FILE="svatantrya.asc"
|
||||
gpg --armor --export "$GPG_KEY" > "$PGP_FILE"
|
||||
echo " Signed: $ASC_FILE"
|
||||
echo " Signed: $SIG_FILE"
|
||||
echo " Public key: $PGP_FILE"
|
||||
|
||||
# ── 7. SHA-256 checksum ────────────────────────────────────────────
|
||||
info "[7/10] Computing SHA-256"
|
||||
SHA256_HASH=$(sha256sum "$ZIP_NAME" | cut -d' ' -f1)
|
||||
echo "${SHA256_HASH} ${ZIP_NAME}" > "${ZIP_NAME}.sha256"
|
||||
echo " SHA-256: ${SHA256_HASH}"
|
||||
|
||||
# ── 8. Pause for Electrum test ─────────────────────────────────────
|
||||
info "[8/10] Test in Electrum (ZIP-FIRST policy)"
|
||||
echo ""
|
||||
echo " ZIP ready: $(pwd)/${ZIP_NAME}"
|
||||
echo ""
|
||||
echo " Install it in Electrum (Tools -> Plugins -> Install from file)."
|
||||
echo " IMPORTANT: fully restart Electrum (not just reload the plugin)."
|
||||
echo ""
|
||||
read -r -p " Does the plugin work correctly in Electrum? [y/N] " CONFIRM
|
||||
case "$CONFIRM" in
|
||||
[yY][eE][sS]|[yY]) echo " Confirmed." ;;
|
||||
*) die "Aborted by user." ;;
|
||||
esac
|
||||
|
||||
# ── 9. Git tag + push ──────────────────────────────────────────────
|
||||
info "[9/10] Creating and pushing tag ${TAG}"
|
||||
ORIGIN_URL="$(git remote get-url origin 2>/dev/null || true)"
|
||||
[ -n "$ORIGIN_URL" ] || die "no git remote 'origin' found"
|
||||
|
||||
GITEA_HOST="$(echo "$ORIGIN_URL" | sed -n 's|https://\([^/]*\)/.*|\1|p')"
|
||||
[ -n "$GITEA_HOST" ] || die "cannot parse Gitea host from origin URL"
|
||||
|
||||
TARGET_REPO="bitcoinafterlife/bal-electrum-plugin"
|
||||
|
||||
# credentials
|
||||
GITEA_USER="${GITEA_USER:-}"
|
||||
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||
if [ -z "$GITEA_USER" ] && [ -z "$GITEA_TOKEN" ]; then
|
||||
if [ -f ~/.git-credentials ]; then
|
||||
CREDS_LINE="$(grep "${GITEA_HOST}" ~/.git-credentials | head -n1)"
|
||||
if [ -n "$CREDS_LINE" ]; then
|
||||
CREDS="$(echo "$CREDS_LINE" | sed -n 's|https://\([^@]*\)@.*|\1|p')"
|
||||
GITEA_USER="$(echo "$CREDS" | cut -d: -f1)"
|
||||
GITEA_PASS="$(echo "$CREDS" | cut -d: -f2-)"
|
||||
GITEA_TOKEN="$GITEA_PASS"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
[ -n "$GITEA_TOKEN" ] || die "GITEA_TOKEN not set and no credentials in ~/.git-credentials"
|
||||
|
||||
API="https://$GITEA_HOST/gitea/api/v1"
|
||||
|
||||
# create annotated tag
|
||||
git tag -d "$TAG" 2>/dev/null || true
|
||||
git tag -a "$TAG" -m "$TAG" HEAD
|
||||
|
||||
# push tag
|
||||
REMOTE_NAME="gitea-target"
|
||||
REMOTE_URL="https://$GITEA_USER:$GITEA_TOKEN@$GITEA_HOST/gitea/$TARGET_REPO.git"
|
||||
git remote rm "$REMOTE_NAME" 2>/dev/null || true
|
||||
git remote add "$REMOTE_NAME" "$REMOTE_URL"
|
||||
echo " Pushing tag ${TAG} to ${TARGET_REPO}..."
|
||||
git push "$REMOTE_NAME" "$TAG" --force
|
||||
|
||||
# ── 10. Create release + upload assets ──────────────────────────────
|
||||
info "[10/10] Creating Gitea release"
|
||||
|
||||
RELEASE_BODY=$(python3 -c "
|
||||
import json
|
||||
|
||||
sha256 = '${SHA256_HASH}'
|
||||
zip_name = '${ZIP_NAME}'
|
||||
asc_name = '${ASC_FILE}'
|
||||
sig_name = '${SIG_FILE}'
|
||||
pgp_file = '${PGP_FILE}'
|
||||
tag = '${TAG}'
|
||||
|
||||
body = f'''Release {tag}
|
||||
|
||||
## SHA-256 Checksum
|
||||
|
||||
\`\`\`
|
||||
{sha256} {zip_name}
|
||||
\`\`\`
|
||||
|
||||
### Verify SHA-256
|
||||
|
||||
\`\`\`bash
|
||||
sha256sum -c {zip_name}.sha256
|
||||
\`\`\`
|
||||
|
||||
## Download
|
||||
|
||||
- \`{zip_name}\` - Plugin BAL {tag}
|
||||
- \`{asc_name}\` - GPG signature (armor)
|
||||
- \`{sig_name}\` - GPG signature (binary)
|
||||
- \`{pgp_file}\` - Signing public key ([also available online](https://bitcoin-after.life/svatantrya.asc))
|
||||
|
||||
## GPG Verification
|
||||
|
||||
### Import the signing key
|
||||
|
||||
\`\`\`bash
|
||||
gpg --fetch-key https://bitcoin-after.life/svatantrya.asc
|
||||
\`\`\`
|
||||
|
||||
Or download \`{pgp_file}\` from the assets above:
|
||||
|
||||
\`\`\`bash
|
||||
gpg --import {pgp_file}
|
||||
\`\`\`
|
||||
|
||||
### Verify the signature (armor)
|
||||
|
||||
\`\`\`bash
|
||||
gpg --verify {asc_name} {zip_name}
|
||||
\`\`\`
|
||||
|
||||
### Verify the signature (binary)
|
||||
|
||||
\`\`\`bash
|
||||
gpg --verify {sig_name} {zip_name}
|
||||
\`\`\`
|
||||
|
||||
Expected output:
|
||||
\`\`\`
|
||||
gpg: Good signature from "Svātantrya <svatantrya@bitcoin-after.life>"
|
||||
\`\`\`
|
||||
|
||||
Fingerprint: \`A847D004DB91610711CA6A0DFE756706E833E0D1\`
|
||||
Public key: https://bitcoin-after.life/svatantrya.asc'''
|
||||
|
||||
print(json.dumps({'body': body}, ensure_ascii=False))
|
||||
")
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: token $GITEA_TOKEN" \
|
||||
-X POST "${API}/repos/${TARGET_REPO}/releases" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"body\":${RELEASE_BODY},\"draft\":false,\"prerelease\":false}")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" != "201" ]; then
|
||||
echo "Error creating release: HTTP $HTTP_CODE"
|
||||
echo "$BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RELEASE_ID=$(echo "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
||||
HTML_URL=$(echo "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin)['html_url'])")
|
||||
echo " Release created: $HTML_URL (ID: $RELEASE_ID)"
|
||||
|
||||
# upload assets
|
||||
for FILE in "$ZIP_NAME" "$ASC_FILE" "$SIG_FILE" "${ZIP_NAME}.sha256" "$PGP_FILE"; do
|
||||
BASENAME=$(basename "$FILE")
|
||||
echo " Uploading $BASENAME ..."
|
||||
UPLOAD=$(curl -s -w "\n%{http_code}" -H "Authorization: token $GITEA_TOKEN" \
|
||||
-X POST "${API}/repos/${TARGET_REPO}/releases/${RELEASE_ID}/assets" \
|
||||
-F "attachment=@${FILE}" -F "name=${BASENAME}")
|
||||
UPLOAD_CODE=$(echo "$UPLOAD" | tail -1)
|
||||
if [ "$UPLOAD_CODE" == "201" ]; then
|
||||
echo " OK ($BASENAME)"
|
||||
else
|
||||
echo " FAILED ($BASENAME) - HTTP $UPLOAD_CODE"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Release: $HTML_URL"
|
||||
echo "Assets:"
|
||||
echo " ${ZIP_NAME}"
|
||||
echo " ${ASC_FILE}"
|
||||
echo " ${SIG_FILE}"
|
||||
echo " ${ZIP_NAME}.sha256"
|
||||
echo " ${PGP_FILE}"
|
||||
echo "SHA-256: ${SHA256_HASH}"
|
||||
30
svatantrya.asc
Normal file
30
svatantrya.asc
Normal file
@@ -0,0 +1,30 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBGfgPmMBCAC4VXQn/ofBGPn/Wr9dF4tM/4uYNcWLvvz+/+TQsCi/bv4GG6jf
|
||||
6Ttlg4TDwqF3JlZ1YfPImcdWKxr9is4fyq12OEZvz12LoFEJG8+0NdJrCoT2sm2f
|
||||
yGmWKgZqRzH9LVBtIOOQIrXF3PdE0X77trWnSFrK/qAv9dszYiVOk9IBwUVI/3Wp
|
||||
PN5EV7zqbCjYvzD0Hxl2sFzZKqsZCsiy70PJtaJKvKISd8RVTNuIiwZj0gu6hCSa
|
||||
ZnBr5SLLr56YO4xaTzYNYh7XIEaQXZTHugEJbwygfZajnJ8gC91wWB3BsxVeHDdm
|
||||
uDy1VGkAs65qvRn9ml5udmnEIPoEsS95HblpABEBAAG0K1N2xIF0YW50cnlhIDxz
|
||||
dmF0YW50cnlhQGJpdGNvaW4tYWZ0ZXIubGlmZT6JAU4EEwEKADgWIQSoR9AE25Fh
|
||||
BxHKag3+dWcG6DPg0QUCZ+A+YwIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAK
|
||||
CRD+dWcG6DPg0coSCACbu3/tqMTwWTqRzXedl6VTGng+qeYfA5NYUaRgZeQYcVWM
|
||||
sUi4dTAthBUxU3axfcu3V/Vkonn/Hrghdjh94lfpNsgdBNi3c2elI1rHT3Yobkj+
|
||||
ZsMEj91VlqV81uPFzfq8a/Pp7RIDhy1FJbIunmjnpD3GeJ7vVt76OOcyjV5hkGR0
|
||||
YJ4JX9O1OOC6wqgR2HVCvXTw/3JhNbj4TS8wr7GGsVWwiotAwZw506vspQRBqeYB
|
||||
T5Wo2lpEQtagWzIHtgy4A2iAoLQ45E0T1lkr+mZa3V7sucS6W/UXI7HTvqC7wbku
|
||||
jef6Hxwzzw83TWqPkd4wywuHsDZ3+DTcIDaqP/ROuQENBGfgPmMBCAC69Y2n2Ogi
|
||||
T7i4Pm4J0cQxLaqwvox3GWSRuBG0QlhsBr0ER5j5fRRDH85P/WyTcvs4/9mIZsSl
|
||||
JyQH/Lfetr/76pFCyc2zhKxxS1miG3RWOuM7BOKbRjjiieBa6XAiyWStKp2ij8a/
|
||||
kpqqgulLe1Tiq2SRPA8etqHGd7oR02fbEvzmsgiVqFOz3/tozp2jdC7zCKnp+XFZ
|
||||
xMKqhIMgfZAxRmVl/qImH944ffcJU6M+qjEL3ENXpuDXpMSWI/indlbK06+R/UPA
|
||||
hOxCOUSRPTeHzhQrJYUgH6Q6Q/cijpTQHVQFFqLXRKGgK7oE1QhmiNGNBeCVF7DP
|
||||
hpcWrnUkY/xFABEBAAGJATYEGAEKACAWIQSoR9AE25FhBxHKag3+dWcG6DPg0QUC
|
||||
Z+A+YwIbDAAKCRD+dWcG6DPg0ToJB/4t2V4FMqd2q00Sd+HmttZoAWNuklui8wO4
|
||||
nrjfh3Rt0ZBYYk+egZXzPx8lr42Ec8T4h24oJPovMlDu1xN9seQDbVaYC1ICVsnp
|
||||
6/yfh+elYT5egaAxm9oP9+lQHBB/qZNKrfAssMuVQOrVh5E+XxSz+KG28dQnCYUT
|
||||
L0k5PCO1f4Jz4XZd5AunVbMQ4J1JawUDoEb/w3Mn9ALDMsdAcOYC6pGhFtV88cqu
|
||||
IO/ekQV+M8LpRwyh+CiPzgqtN3Z09wHLXFUJYBixXrYbXxAbSqe0PhqAhEKApk2c
|
||||
4vVkSTAi+bNpkt0QgJ194iTyK20jVw3/roq7sUtDD4FrUoQb7llP
|
||||
=6EE4
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -206,7 +206,7 @@ def test_default_will_settings_relative():
|
||||
|
||||
def test_default_will_settings():
|
||||
settings = BalPlugin.default_will_settings()
|
||||
assert settings["baltx_fees"] == 100
|
||||
assert settings["baltx_fees"] == 20
|
||||
assert "threshold" in settings
|
||||
assert "locktime" in settings
|
||||
# threshold/locktime should be absolute timestamps
|
||||
@@ -228,7 +228,7 @@ def test_validate_will_settings():
|
||||
# Note: passing None triggers `will_settings = []` which then fails
|
||||
# on .get(). This is a latent bug — test passing a dict directly
|
||||
result = BalPlugin.validate_will_settings(None, {"baltx_fees": 0})
|
||||
assert result["baltx_fees"] == 100
|
||||
assert result["baltx_fees"] == 20
|
||||
|
||||
# normal settings unchanged
|
||||
input_settings = {"baltx_fees": 50, "threshold": 1700000000, "locktime": 1800000000}
|
||||
|
||||
Reference in New Issue
Block a user