21 Commits

Author SHA1 Message Date
Markus F.J. Busche
9a84d7da2e docs: document systemd-based auto-disable of Tailscale Funnel on boot 2025-09-21 15:01:10 +02:00
Markus F.J. Busche
eef0bb13c8 docs: link to latest Releases on Gitea; release notes updated (en) 2025-09-21 13:36:33 +02:00
Markus F.J. Busche
876048f60e chore(release): release_gitea.sh (Tag+Release+Assets) hinzugefügt 2025-09-21 13:24:40 +02:00
Markus F.J. Busche
4ffb6f82b9 Merge branch 'navbar_step1_static': Navbar-Button/Status + Build-Skript + v0.1.6.3 2025-09-21 13:21:40 +02:00
Markus F.J. Busche
a68b7c6463 chore(release): bump version to 0.1.6.3 (setup.py, update.json) 2025-09-21 13:20:26 +02:00
Markus F.J. Busche
73c5da01f9 Navbar: Farbkennung am Button (tsf-enabled/tsf-disabled) mit Live-Umschalten 2025-09-21 13:14:55 +02:00
Markus F.J. Busche
f225acc752 Navbar: sofortiger Status-Refresh nach Toggle (UX-Fix) 2025-09-21 13:09:14 +02:00
Markus F.J. Busche
13dd1dbd44 Navbar step3: Toggle Enable/Disable im Dropdown mit einfachem Confirm und Auto-Refresh 2025-09-21 13:03:49 +02:00
Markus F.J. Busche
f5a34df9e5 Navbar step2: minimaler Status im Dropdown (GET /plugin/tailscale_funnel/status), Open-Link bei Enabled 2025-09-21 12:52:47 +02:00
Markus F.J. Busche
5e448e8041 Navbar: Dropdown korrekt positionieren (li.dropdown) 2025-09-21 12:40:37 +02:00
Markus F.J. Busche
7bffb76749 build(pkg): MANIFEST.in korrigiert (static/templates unter Paketpfad); Plugin requirements.txt belassen; Struktur aufgeräumt 2025-09-21 12:33:15 +02:00
Markus F.J. Busche
dd89c5f650 build: setup.py ohne OctoPrint-Build-Abhängigkeit (dict_merge Fallback); project requirements.txt; build script nutzt requirements.txt 2025-09-21 12:28:43 +02:00
Markus F.J. Busche
ac3298c40e build: build_plugin.sh wiederhergestellt (venv, optionaler Versionsbump, ZIP) 2025-09-21 12:25:42 +02:00
Markus F.J. Busche
708c652d21 Navbar step1: statischer Navbar-Eintrag ohne JS-Bindings hinzugefügt 2025-09-21 12:21:26 +02:00
Markus F.J. Busche
fd5ff20743 Add disclaimer 2025-09-20 19:31:50 +02:00
Markus F.J. Busche
87449641f6 chore: remove scripts/upload_asset_direct.sh (deprecated) 2025-09-20 19:29:49 +02:00
Markus F.J. Busche
731cd207dd chore: stage remaining changes 2025-09-20 18:29:54 +02:00
Markus F.J. Busche
a5489de212 docs: korrigiere Screenshot-Dateiname auf .png 2025-09-20 18:24:47 +02:00
Markus F.J. Busche
c0d4448dc1 docs: symlink docs/ -> octoprint_tailscale_funnel/octoprint_tailscale_funnel/docs 2025-09-20 18:24:24 +02:00
Markus F.J. Busche
0d43e181c8 docs: Screenshot der Funnel-Einstellungen in README verlinkt 2025-09-20 18:18:37 +02:00
Markus F.J. Busche
f60068b01f release: update scripts/release_gitea.sh to jq-based flow; skip existing assets 2025-09-20 18:13:22 +02:00
15 changed files with 491 additions and 200 deletions

1
docs Symbolic link
View File

@@ -0,0 +1 @@
octoprint_tailscale_funnel/octoprint_tailscale_funnel/docs

View File

@@ -1,7 +1,6 @@
include README.md
include LICENSE
include requirements.txt
include setup.py
recursive-include static *
recursive-include templates *
recursive-include octoprint_tailscale_funnel/static *
recursive-include octoprint_tailscale_funnel/templates *
recursive-include tests *

View File

@@ -2,6 +2,8 @@
This plugin makes your OctoPrint instance accessible from anywhere via Tailscale Funnel, without needing to configure port forwarding, dynamic DNS, or complex firewall settings.
Disclaimer: *This plugin was partially vibe-coded*.
## Features
* Enable/disable Tailscale Funnel access directly from OctoPrint's settings
@@ -9,6 +11,10 @@ This plugin makes your OctoPrint instance accessible from anywhere via Tailscale
* Display the public URL for accessing OctoPrint remotely
* Configure the port to expose via Funnel
## Screenshot
![Tailscale Funnel Settings](octoprint_tailscale_funnel/octoprint_tailscale_funnel/docs/screenshots/funnel-settings.png)
## Requirements
* OctoPrint 1.3.0 or higher
@@ -24,6 +30,10 @@ This plugin makes your OctoPrint instance accessible from anywhere via Tailscale
5. Configure the plugin settings in OctoPrint's settings panel
6. Enable Funnel through the plugin interface
### Latest Release
Get the latest packaged release (wheel, sdist, zip) from Gitea: [Releases](https://gitea.elpatron.me/elpatron/octo-funnel/releases)
## Building from Source
If you want to build the plugin from source, please refer to the [BUILDING.md](BUILDING.md) file for detailed instructions.
@@ -75,6 +85,43 @@ sudo tailscale funnel reset
Enabling Funnel makes your OctoPrint instance accessible from the public internet. Only enable it when needed and disable it when finished. The plugin will show a confirmation dialog before enabling Funnel if the "Confirm Enable" option is checked.
### Disable Funnel automatically on system boot (systemd)
If you prefer to make sure the Tailscale Funnel is always off after a system reboot (independent of the plugin), you can add a small systemd unit that disables Funnel shortly after boot:
1) Create unit file:
```bash
sudo tee /etc/systemd/system/octoprint-tailscale-funnel-off.service >/dev/null <<'UNIT'
[Unit]
Description=Disable Tailscale Funnel after boot
After=network-online.target tailscaled.service
Wants=network-online.target
[Service]
Type=oneshot
User=octoprint
ExecStart=/usr/bin/tailscale funnel reset
# Optional: also turn off possible serve mappings on common ports
ExecStart=/usr/bin/tailscale serve --http=80 off
ExecStart=/usr/bin/tailscale serve --https=80 off
[Install]
WantedBy=multi-user.target
UNIT
```
2) Enable and test:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now octoprint-tailscale-funnel-off.service
sudo systemctl status octoprint-tailscale-funnel-off.service
```
Notes:
- Adjust `User=octoprint` if your OctoPrint runs under a different user (e.g. `pi`).
- If sudo is required for tailscale on your system, add the corresponding sudoers entry as described above.
- You can add a short delay by inserting `ExecStartPre=/bin/sleep 5` if Tailscale comes up late during boot.
## API Endpoints
The plugin exposes the following API endpoints:

View File

@@ -91,7 +91,7 @@ class TailscaleFunnelPlugin(octoprint.plugin.StartupPlugin,
def get_assets(self):
return dict(
js=["js/tailscale_funnel.js"],
js=["js/tailscale_funnel.js", "js/tailscale_funnel_navbar.js"],
css=["css/tailscale_funnel.css"],
less=["less/tailscale_funnel.less"]
)
@@ -100,7 +100,8 @@ class TailscaleFunnelPlugin(octoprint.plugin.StartupPlugin,
def get_template_configs(self):
return [
dict(type="settings", custom_bindings=True, template="tailscale_funnel_settings.jinja2")
dict(type="settings", custom_bindings=True, template="tailscale_funnel_settings.jinja2"),
dict(type="navbar", name="Funnel", custom_bindings=False, template="tailscale_funnel_navbar.jinja2")
]
##~~ BlueprintPlugin mixin

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -38,4 +38,15 @@
background-color: #fcf8e3;
border-color: #faebcc;
color: #8a6d3b;
}
/* Navbar Button Farbkennung */
#navbar_plugin_tailscale_funnel > a.tsf-enabled {
background-color: #5cb85c;
color: #fff;
}
#navbar_plugin_tailscale_funnel > a.tsf-disabled {
background-color: #777;
color: #fff;
}

View File

@@ -0,0 +1,105 @@
$(function() {
var lastEnabled = null;
function refreshNavbarStatus() {
var $status = $("#tsf_nav_status");
var $openLi = $("#tsf_nav_open_li");
var $open = $("#tsf_nav_open");
var $toggleText = $("#tsf_nav_toggle_text");
var $btn = $("#navbar_plugin_tailscale_funnel > a");
if ($status.length === 0) return;
$status.text("Checking...");
$.ajax({
url: PLUGIN_BASEURL + "tailscale_funnel/status",
type: "GET",
dataType: "json",
success: function(resp) {
if (resp && resp.status === "success") {
var enabled = !!(resp.data && resp.data.funnel_enabled);
var url = resp.data && resp.data.public_url ? resp.data.public_url : "";
$status.text(enabled ? "Enabled" : "Disabled");
$toggleText.text(enabled ? "Disable" : "Enable");
$btn.toggleClass('tsf-enabled', enabled).toggleClass('tsf-disabled', !enabled);
lastEnabled = enabled;
if (enabled && url) {
$open.attr("href", url);
$openLi.removeClass("hidden");
} else {
$open.attr("href", "#");
$openLi.addClass("hidden");
}
} else {
$status.text("Error");
$toggleText.text("Enable");
$btn.removeClass('tsf-enabled tsf-disabled');
$openLi.addClass("hidden");
}
},
error: function() {
$status.text("Error");
$toggleText.text("Enable");
$btn.removeClass('tsf-enabled tsf-disabled');
$openLi.addClass("hidden");
}
});
}
// Refresh when dropdown opens, and on click of Refresh
$(document).on('show.bs.dropdown', '#navbar_plugin_tailscale_funnel', refreshNavbarStatus);
$(document).on('click', '#tsf_nav_refresh', function(e) {
e.preventDefault();
refreshNavbarStatus();
});
$(document).on('click', '#tsf_nav_toggle', function(e) {
e.preventDefault();
var enable = !(lastEnabled === true);
// Optional simpler confirm: nur beim Aktivieren
if (enable) {
var c = window.confirm("Enabling Funnel will make your OctoPrint instance accessible from the public internet. Continue?");
if (!c) return;
}
var action = enable ? 'enable' : 'disable';
var $status = $("#tsf_nav_status");
var $toggleText = $("#tsf_nav_toggle_text");
var $btn = $("#navbar_plugin_tailscale_funnel > a");
$status.text(enable ? 'Enabling...' : 'Disabling...');
$.ajax({
url: PLUGIN_BASEURL + 'tailscale_funnel/' + action,
type: 'POST',
dataType: 'json',
success: function(resp) {
if (resp && resp.status === 'success') {
if (enable) {
$status.text('Enabled');
$toggleText.text('Disable');
$btn.addClass('tsf-enabled').removeClass('tsf-disabled');
var url = resp.data && resp.data.public_url ? resp.data.public_url : '';
if (url) {
$("#tsf_nav_open").attr('href', url);
$("#tsf_nav_open_li").removeClass('hidden');
}
lastEnabled = true;
} else {
$status.text('Disabled');
$toggleText.text('Enable');
$btn.addClass('tsf-disabled').removeClass('tsf-enabled');
$("#tsf_nav_open").attr('href', '#');
$("#tsf_nav_open_li").addClass('hidden');
lastEnabled = false;
}
// Finaler Abgleich mit Backendstatus
setTimeout(refreshNavbarStatus, 250);
} else {
$status.text('Error');
$btn.removeClass('tsf-enabled tsf-disabled');
}
},
error: function() {
$status.text('Error');
$btn.removeClass('tsf-enabled tsf-disabled');
}
});
});
});

View File

@@ -0,0 +1,14 @@
<li id="navbar_plugin_tailscale_funnel" class="dropdown">
<a class="dropdown-toggle" href="#" data-toggle="dropdown" title="Tailscale Funnel">
<i class="fas fa-share-square"></i>
<span class="visible-lg">Funnel</span>
</a>
<ul class="dropdown-menu">
<li class="disabled"><a href="#"><strong>Status:</strong> <span id="tsf_nav_status">Checking...</span></a></li>
<li id="tsf_nav_open_li" class="hidden"><a id="tsf_nav_open" href="#" target="_blank" rel="noopener"><i class="fas fa-external-link-alt"></i> Open</a></li>
<li><a id="tsf_nav_toggle" href="#"><i class="fas fa-toggle-on"></i> <span id="tsf_nav_toggle_text">Enable</span></a></li>
<li class="divider"></li>
<li><a id="tsf_nav_refresh" href="#"><i class="fas fa-sync"></i> Refresh</a></li>
</ul>
</li>

View File

@@ -1 +1,2 @@
# Runtime dependency for installation via OctoPrint's Plugin Manager
OctoPrint>=1.3.0

View File

@@ -14,7 +14,7 @@ plugin_package = "octoprint_tailscale_funnel"
plugin_name = "OctoPrint-Tailscale-Funnel"
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
plugin_version = "0.1.6.2"
plugin_version = "0.1.6.3"
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
# module
@@ -88,7 +88,18 @@ setup_parameters = octoprint_setuptools.create_plugin_setup_parameters(
)
if len(additional_setup_parameters):
from octoprint.util import dict_merge
try:
from octoprint.util import dict_merge
except Exception:
# Fallback to allow building without the octoprint package installed
def dict_merge(a, b):
result = dict(a)
for k, v in b.items():
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
result[k] = dict_merge(result[k], v)
else:
result[k] = v
return result
setup_parameters = dict_merge(setup_parameters, additional_setup_parameters)
setup(**setup_parameters)

View File

@@ -1,4 +1,4 @@
{
"version": "0.1.6.2"
"version": "0.1.6.3"
}

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
setuptools>=68
wheel>=0.41
build>=1.0
packaging>=23
# Kompatible verfügbare Version laut Index
octoprint_setuptools==1.0.3

147
scripts/build_plugin.sh Executable file
View File

@@ -0,0 +1,147 @@
#!/usr/bin/env bash
set -euo pipefail
# Usage: scripts/build_plugin.sh [VERSION]
# - Läuft im Projekt-.venv
# - Optional: VERSION (z.B. 0.1.6.4). Wenn gesetzt, wird setup.py gepatcht.
# - Baut wheel + sdist und erstellt zusätzlich ein "normales" ZIP der sdist-Struktur.
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
PLUGIN_DIR="$ROOT_DIR/octoprint_tailscale_funnel"
DIST_DIR="$PLUGIN_DIR/dist"
VENV_DIR="$ROOT_DIR/.venv"
VERSION_INPUT="${1:-}"
echo "Project root: $ROOT_DIR"
echo "Plugin dir: $PLUGIN_DIR"
echo "Venv dir: $VENV_DIR"
# Ensure venv
if [[ ! -x "$VENV_DIR/bin/python" ]]; then
echo "Creating venv at $VENV_DIR"
python3 -m venv "$VENV_DIR"
fi
if [[ -f "$ROOT_DIR/requirements.txt" ]]; then
"$VENV_DIR/bin/pip" install -q --upgrade pip
"$VENV_DIR/bin/pip" install -q -r "$ROOT_DIR/requirements.txt"
else
"$VENV_DIR/bin/pip" install -q --upgrade pip setuptools wheel build packaging octoprint_setuptools
fi
# Optional: bump version
if [[ -n "$VERSION_INPUT" ]]; then
echo "Setting version to $VERSION_INPUT"
"$VENV_DIR/bin/python" - "$VERSION_INPUT" "$PLUGIN_DIR/setup.py" <<'PY'
import sys, re, pathlib
ver = sys.argv[1]
setup_path = pathlib.Path(sys.argv[2])
text = setup_path.read_text()
new_text = re.sub(r'^(plugin_version\s*=\s*")([^"]*)(")', lambda m: m.group(1) + ver + m.group(3), text, flags=re.M)
setup_path.write_text(new_text)
print("Updated version to", ver)
PY
fi
# Clean dist
mkdir -p "$DIST_DIR"
rm -f "$DIST_DIR"/*
# Build wheel + sdist
echo "Building (wheel + sdist)..."
"$VENV_DIR/bin/python" -m build --no-isolation "$PLUGIN_DIR"
# Create additional plain ZIP from sdist content (flat source zip)
SDIST_TGZ=$(ls -1 "$DIST_DIR"/*.tar.gz | tail -n1 || true)
if [[ -n "$SDIST_TGZ" ]]; then
echo "Creating plain ZIP from sdist: $SDIST_TGZ"
TMP_DIR="$(mktemp -d)"
tar -xzf "$SDIST_TGZ" -C "$TMP_DIR"
SRC_DIR="$(find "$TMP_DIR" -maxdepth 1 -type d -name 'octoprint_tailscale_funnel-*' | head -n1)"
if [[ -n "$SRC_DIR" ]]; then
ZIP_NAME="$(basename "$SRC_DIR").zip"
(cd "$SRC_DIR" && zip -rq "$DIST_DIR/$ZIP_NAME" .)
echo "Created: $DIST_DIR/$ZIP_NAME"
else
echo "WARN: Could not find extracted sdist directory to zip"
fi
rm -rf "$TMP_DIR"
else
echo "WARN: No sdist .tar.gz found, skipping plain ZIP creation"
fi
echo "Artifacts in $DIST_DIR:"
ls -lah "$DIST_DIR"
#!/usr/bin/env bash
set -euo pipefail
# Usage: scripts/build_plugin.sh [VERSION]
# - Läuft im Projekt-.venv
# - Optional: VERSION (z.B. 0.1.6.4). Wenn gesetzt, wird setup.py gepatcht.
# - Baut wheel + sdist und erstellt zusätzlich ein "normales" ZIP der sdist-Struktur.
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
PLUGIN_DIR="$ROOT_DIR/octoprint_tailscale_funnel"
DIST_DIR="$PLUGIN_DIR/dist"
VENV_DIR="$ROOT_DIR/.venv"
VERSION_INPUT="${1:-}"
echo "Project root: $ROOT_DIR"
echo "Plugin dir: $PLUGIN_DIR"
echo "Venv dir: $VENV_DIR"
# Ensure venv
if [[ ! -x "$VENV_DIR/bin/python" ]]; then
echo "Creating venv at $VENV_DIR"
python3 -m venv "$VENV_DIR"
fi
"$VENV_DIR/bin/pip" install -q --upgrade pip setuptools wheel build packaging octoprint_setuptools
# Optional: bump version
if [[ -n "$VERSION_INPUT" ]]; then
echo "Setting version to $VERSION_INPUT"
"$VENV_DIR/bin/python" - "$VERSION_INPUT" "$PLUGIN_DIR/setup.py" <<'PY'
import sys, re, pathlib
ver = sys.argv[1]
setup_path = pathlib.Path(sys.argv[2])
text = setup_path.read_text()
new_text = re.sub(r'^(plugin_version\s*=\s*")([^"]*)(")', lambda m: m.group(1) + ver + m.group(3), text, flags=re.M)
setup_path.write_text(new_text)
print("Updated version to", ver)
PY
fi
# Clean dist
mkdir -p "$DIST_DIR"
rm -f "$DIST_DIR"/*
# Build wheel + sdist
echo "Building (wheel + sdist)..."
"$VENV_DIR/bin/python" -m build --no-isolation "$PLUGIN_DIR"
# Create additional plain ZIP from sdist content (flat source zip)
SDIST_TGZ=$(ls -1 "$DIST_DIR"/*.tar.gz | tail -n1 || true)
if [[ -n "$SDIST_TGZ" ]]; then
echo "Creating plain ZIP from sdist: $SDIST_TGZ"
TMP_DIR="$(mktemp -d)"
tar -xzf "$SDIST_TGZ" -C "$TMP_DIR"
SRC_DIR="$(find "$TMP_DIR" -maxdepth 1 -type d -name 'octoprint_tailscale_funnel-*' | head -n1)"
if [[ -n "$SRC_DIR" ]]; then
ZIP_NAME="$(basename "$SRC_DIR").zip"
(cd "$SRC_DIR" && zip -rq "$DIST_DIR/$ZIP_NAME" .)
echo "Created: $DIST_DIR/$ZIP_NAME"
else
echo "WARN: Could not find extracted sdist directory to zip"
fi
rm -rf "$TMP_DIR"
else
echo "WARN: No sdist .tar.gz found, skipping plain ZIP creation"
fi
echo "Artifacts in $DIST_DIR:"
ls -lah "$DIST_DIR"

214
scripts/release_gitea.sh Normal file → Executable file
View File

@@ -1,6 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
# Usage: scripts/release_gitea.sh <version> [--draft] [--prerelease]
# Env: liest automatisch $ROOT_DIR/.env (GITEA_TOKEN, GITEA_BASE, OWNER, REPO)
# Falls nicht gesetzt, werden GITEA_BASE/OWNER/REPO aus der Git-Remote-URL abgeleitet.
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <version> [--draft] [--prerelease]" >&2
exit 1
fi
VERSION="$1"
shift || true
DRAFT=false
PRERELEASE=false
for arg in "$@"; do
case "$arg" in
--draft) DRAFT=true ;;
--prerelease) PRERELEASE=true ;;
esac
done
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
# Load .env if present
if [[ -f "$ROOT_DIR/.env" ]]; then
set -a
# shellcheck disable=SC1090
. "$ROOT_DIR/.env"
set +a
fi
# Map alternative token variable name
if [[ -z "${GITEA_TOKEN:-}" && -n "${GITEA_API_TOKEN:-}" ]]; then
GITEA_TOKEN="$GITEA_API_TOKEN"
fi
# Map API URL to base if provided
if [[ -z "${GITEA_BASE:-}" && -n "${GITEA_API_URL:-}" ]]; then
# strip trailing /api/... from URL
GITEA_BASE="${GITEA_API_URL%%/api/*}"
fi
# Map owner/repo alternative names
if [[ -z "${OWNER:-}" && -n "${GITEA_OWNER:-}" ]]; then
OWNER="$GITEA_OWNER"
fi
if [[ -z "${REPO:-}" && -n "${GITEA_REPO:-}" ]]; then
REPO="$GITEA_REPO"
fi
# Derive defaults from git remote if not provided
if [[ -z "${GITEA_BASE:-}" || -z "${OWNER:-}" || -z "${REPO:-}" ]]; then
ORIGIN_URL=$(git -C "$ROOT_DIR" remote get-url origin 2>/dev/null || true)
if [[ "$ORIGIN_URL" =~ ^https?://([^/]+)/([^/]+)/([^/]+?)(\.git)?$ ]]; then
: "${GITEA_BASE:="https://${BASH_REMATCH[1]}"}"
: "${OWNER:=${BASH_REMATCH[2]}}"
: "${REPO:=${BASH_REMATCH[3]}}"
fi
fi
: "${GITEA_TOKEN:?Set GITEA_TOKEN (in .env oder Umgebung)}"
: "${GITEA_BASE:?Set GITEA_BASE (z. B. https://gitea.elpatron.me)}"
: "${OWNER:?Set OWNER (z. B. elpatron)}"
: "${REPO:?Set REPO (z. B. octo-funnel)}"
DIST_DIR="$ROOT_DIR/octoprint_tailscale_funnel/dist"
TAG="v${VERSION}"
echo "Creating git tag ${TAG} and pushing..."
git tag -f "${TAG}"
git push -f origin "${TAG}"
echo "Creating Gitea release ${TAG}..."
BODY=$(cat <<EOF
Release ${TAG}
Changes:
- Navbar: Statusanzeige, Toggle, Farbkennung
- Build-Skript & Quick-Build Docs
- Version ${VERSION}
EOF
)
CREATE_PAYLOAD=$(jq -n \
--arg tag_name "${TAG}" \
--arg name "${TAG}" \
--arg body "${BODY}" \
--argjson draft ${DRAFT} \
--argjson prerelease ${PRERELEASE} \
'{tag_name:$tag_name, name:$name, body:$body, draft:$draft, prerelease:$prerelease}')
RELEASE_JSON=$(curl -sS -X POST "${GITEA_BASE}/api/v1/repos/${OWNER}/${REPO}/releases" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${CREATE_PAYLOAD}")
UPLOAD_URL=$(echo "$RELEASE_JSON" | jq -r .upload_url)
ID=$(echo "$RELEASE_JSON" | jq -r .id)
if [[ -z "$ID" || "$ID" == "null" ]]; then
echo "Failed to create release: $RELEASE_JSON" >&2
exit 1
fi
echo "Release created: ID=$ID"
echo "Using: GITEA_BASE=$GITEA_BASE OWNER=$OWNER REPO=$REPO"
function upload_asset() {
local file="$1"
local name
name=$(basename "$file")
echo "Uploading asset: $name"
curl -sS -X POST "${GITEA_BASE}/api/v1/repos/${OWNER}/${REPO}/releases/${ID}/assets?name=${name}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H 'Content-Type: application/octet-stream' \
--data-binary @"$file" > /dev/null
}
upload_asset "$DIST_DIR/octoprint_tailscale_funnel-${VERSION}-py3-none-any.whl"
upload_asset "$DIST_DIR/octoprint_tailscale_funnel-${VERSION}.tar.gz"
upload_asset "$DIST_DIR/octoprint_tailscale_funnel-${VERSION}.zip"
echo "Release ${TAG} created and assets uploaded."
#!/usr/bin/env bash
set -euo pipefail
# Load .env if present
if [ -f "$(dirname "$0")/../.env" ]; then
set -a
@@ -54,31 +179,11 @@ fi
# Try to fetch existing release by tag first
get_resp=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/tags/${TAG}" || true)
rel_id=$(python3 - <<'PY'
import sys, json
data=sys.stdin.read().strip()
try:
obj=json.loads(data) if data else {}
print(obj.get('id',''))
except Exception:
print('')
PY
<<<"$get_resp")
rel_id=$(echo "$get_resp" | jq -r '.id // empty')
if [ -z "$rel_id" ]; then
# Build minimal JSON payload (use existing tag)
create_payload=$(REL_TAG="$TAG" REL_NAME="$NAME" REL_BODY_TXT="$BODY" python3 - <<'PY'
import json, os
payload = {
"tag_name": os.environ["REL_TAG"],
"name": os.environ["REL_NAME"],
"body": os.environ["REL_BODY_TXT"],
"draft": False,
"prerelease": False
}
print(json.dumps(payload))
PY
)
create_payload=$(jq -n --arg tag "$TAG" --arg name "$NAME" --arg body "$BODY" '{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false}')
create_resp=$(curl -sS -X POST \
-H 'Content-Type: application/json' \
@@ -87,35 +192,13 @@ PY
"${API_URL}/repos/${OWNER}/${REPO}/releases" || true)
# Extract id from create response
rel_id=$(python3 - <<'PY'
import sys, json
data=sys.stdin.read().strip()
try:
obj=json.loads(data) if data else {}
print(obj.get('id',''))
except Exception:
print('')
PY
<<<"$create_resp")
rel_id=$(echo "$create_resp" | jq -r '.id // empty')
fi
# Fallback: search releases list for matching tag if still empty
if [ -z "$rel_id" ]; then
list_resp=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases?limit=100")
rel_id=$(python3 - <<'PY'
import sys, json, os
data=sys.stdin.read().strip()
try:
arr=json.loads(data) if data else []
tag=os.environ.get('TAG')
for rel in arr:
if rel.get('tag_name')==tag:
print(rel.get('id',''))
break
except Exception:
pass
PY
<<<"$list_resp")
rel_id=$(echo "$list_resp" | jq -r --arg tag "$TAG" '[.[] | select(.tag_name==$tag)][0].id // empty')
fi
if [ -z "$rel_id" ]; then
@@ -124,37 +207,18 @@ if [ -z "$rel_id" ]; then
exit 1
fi
# Delete existing asset with same name (if any)
# Check if asset exists (skip upload if present)
assets_json=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets")
asset_id=$(python3 - <<'PY'
import sys, json, os
name=os.environ['ASSET_NAME']
try:
arr=json.loads(sys.stdin.read())
for a in arr:
if a.get('name')==name:
print(a.get('id',''))
break
except Exception:
pass
PY
<<<"$assets_json")
asset_exists=$(echo "$assets_json" | jq -r --arg name "$ASSET_NAME" 'any(.[]; .name==$name)')
if [ -n "$asset_id" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets/${asset_id}" >/dev/null || true
if [ "$asset_exists" = "true" ]; then
echo "Asset already exists, skipping upload: ${ASSET_NAME}"
else
echo "Uploading asset: ${ASSET_NAME}"
upload_resp=$(curl -sS -H "Authorization: token ${TOKEN}" -F attachment=@"${ASSET_PATH}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets?name=${ASSET_NAME}")
html_url=$(echo "$upload_resp" | jq -r '.browser_download_url // empty')
echo "Asset uploaded: ${html_url}"
fi
# Upload asset
upload_resp=$(curl -sS -H "Authorization: token ${TOKEN}" -F attachment=@"${ASSET_PATH}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets?name=${ASSET_NAME}")
html_url=$(python3 - <<'PY'
import sys, json
try:
print(json.loads(sys.stdin.read()).get('browser_download_url',''))
except Exception:
print("")
PY
<<<"$upload_resp")
echo "Release ${TAG} created (id=${rel_id}). Asset uploaded: ${html_url}"
echo "Release ${TAG} ready (id=${rel_id})."

View File

@@ -1,117 +0,0 @@
#!/usr/bin/env bash
set -e
set +u
# Load .env
if [ -f "$(dirname "$0")/../.env" ]; then
# shellcheck disable=SC1091
. "$(dirname "$0")/../.env"
fi
API_URL=${GITEA_API_URL:-"https://gitea.elpatron.me/api/v1"}
OWNER=${GITEA_OWNER:-"elpatron"}
REPO=${GITEA_REPO:-"octo-funnel"}
TOKEN=${GITEA_API_TOKEN:-""}
TAG=${1:-v0.1.6.1}
ASSET_PATH=${2:-"$(pwd)/octoprint_tailscale_funnel/dist/OctoPrint-Tailscale-Funnel-0.1.6.1.zip"}
if [ -z "$TOKEN" ]; then
echo "GITEA_API_TOKEN not set" >&2
exit 1
fi
if [ ! -f "$ASSET_PATH" ]; then
echo "Asset not found: $ASSET_PATH" >&2
exit 1
fi
ASSET_NAME=$(basename "$ASSET_PATH")
# Get release by tag
REL_JSON=$(curl -sS -H "Authorization: token $TOKEN" "$API_URL/repos/$OWNER/$REPO/releases/tags/$TAG" || true)
REL_ID=$(python3 - <<'PY'
import sys, json
s=sys.stdin.read().strip()
try:
d=json.loads(s) if s else {}
print(d.get('id',''))
except Exception:
print('')
PY
<<<"$REL_JSON")
# Fallback list search
if [ -z "$REL_ID" ]; then
LIST=$(curl -sS -H "Authorization: token $TOKEN" "$API_URL/repos/$OWNER/$REPO/releases?limit=100")
REL_ID=$(TAG="$TAG" python3 - <<'PY'
import os, sys, json
tag=os.environ['TAG']
try:
arr=json.loads(sys.stdin.read())
for r in arr:
if r.get('tag_name')==tag:
print(r.get('id',''))
break
except Exception:
print('')
PY
<<<"$LIST")
fi
# Create release if still missing
if [ -z "$REL_ID" ]; then
PAYLOAD=$(TAG="$TAG" python3 - <<'PY'
import json, os
print(json.dumps({
'tag_name': os.environ['TAG'],
'name': os.environ['TAG'],
'body': 'Automated release'
}))
PY
)
CREATE=$(curl -sS -X POST -H 'Content-Type: application/json' -H "Authorization: token $TOKEN" -d "$PAYLOAD" "$API_URL/repos/$OWNER/$REPO/releases")
REL_ID=$(python3 - <<'PY'
import sys, json
try:
d=json.loads(sys.stdin.read())
print(d.get('id',''))
except Exception:
print('')
PY
<<<"$CREATE")
fi
if [ -z "$REL_ID" ]; then
echo "Failed to resolve release id for tag $TAG" >&2
exit 1
fi
# Check existing assets
ASSETS=$(curl -sS -H "Authorization: token $TOKEN" "$API_URL/repos/$OWNER/$REPO/releases/$REL_ID/assets")
HAS=$(NAME="$ASSET_NAME" python3 - <<'PY'
import os, sys, json
name=os.environ['NAME']
try:
arr=json.loads(sys.stdin.read())
print(any(a.get('name')==name for a in arr))
except Exception:
print(False)
PY
<<<"$ASSETS")
if [ "$HAS" = "True" ]; then
echo "Asset already exists: $ASSET_NAME"
else
echo "Uploading asset: $ASSET_NAME"
curl -sS -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/octet-stream" \
--data-binary @"$ASSET_PATH" \
"$API_URL/repos/$OWNER/$REPO/releases/$REL_ID/assets?name=$ASSET_NAME" >/dev/null
echo "Upload done."
fi
DL_URL="https://gitea.elpatron.me/$OWNER/$REPO/releases/download/$TAG/$ASSET_NAME"
CODE=$(curl -s -o /dev/null -w '%{http_code}' "$DL_URL")
echo "DOWNLOAD_URL=$DL_URL"
echo "HTTP_STATUS=$CODE"