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 README.md
include LICENSE include LICENSE
include requirements.txt
include setup.py include setup.py
recursive-include static * recursive-include octoprint_tailscale_funnel/static *
recursive-include templates * recursive-include octoprint_tailscale_funnel/templates *
recursive-include tests * 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. 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 ## Features
* Enable/disable Tailscale Funnel access directly from OctoPrint's settings * 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 * Display the public URL for accessing OctoPrint remotely
* Configure the port to expose via Funnel * Configure the port to expose via Funnel
## Screenshot
![Tailscale Funnel Settings](octoprint_tailscale_funnel/octoprint_tailscale_funnel/docs/screenshots/funnel-settings.png)
## Requirements ## Requirements
* OctoPrint 1.3.0 or higher * 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 5. Configure the plugin settings in OctoPrint's settings panel
6. Enable Funnel through the plugin interface 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 ## Building from Source
If you want to build the plugin from source, please refer to the [BUILDING.md](BUILDING.md) file for detailed instructions. 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. 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 ## API Endpoints
The plugin exposes the following API endpoints: The plugin exposes the following API endpoints:

View File

@@ -91,7 +91,7 @@ class TailscaleFunnelPlugin(octoprint.plugin.StartupPlugin,
def get_assets(self): def get_assets(self):
return dict( return dict(
js=["js/tailscale_funnel.js"], js=["js/tailscale_funnel.js", "js/tailscale_funnel_navbar.js"],
css=["css/tailscale_funnel.css"], css=["css/tailscale_funnel.css"],
less=["less/tailscale_funnel.less"] less=["less/tailscale_funnel.less"]
) )
@@ -100,7 +100,8 @@ class TailscaleFunnelPlugin(octoprint.plugin.StartupPlugin,
def get_template_configs(self): def get_template_configs(self):
return [ 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 ##~~ BlueprintPlugin mixin

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -39,3 +39,14 @@
border-color: #faebcc; border-color: #faebcc;
color: #8a6d3b; 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 OctoPrint>=1.3.0

View File

@@ -14,7 +14,7 @@ plugin_package = "octoprint_tailscale_funnel"
plugin_name = "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 # 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 # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
# module # module
@@ -88,7 +88,18 @@ setup_parameters = octoprint_setuptools.create_plugin_setup_parameters(
) )
if len(additional_setup_parameters): if len(additional_setup_parameters):
try:
from octoprint.util import dict_merge 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_parameters = dict_merge(setup_parameters, additional_setup_parameters)
setup(**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 #!/usr/bin/env bash
set -euo pipefail 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 # Load .env if present
if [ -f "$(dirname "$0")/../.env" ]; then if [ -f "$(dirname "$0")/../.env" ]; then
set -a set -a
@@ -54,31 +179,11 @@ fi
# Try to fetch existing release by tag first # 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) get_resp=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/tags/${TAG}" || true)
rel_id=$(python3 - <<'PY' rel_id=$(echo "$get_resp" | jq -r '.id // empty')
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")
if [ -z "$rel_id" ]; then if [ -z "$rel_id" ]; then
# Build minimal JSON payload (use existing tag) # Build minimal JSON payload (use existing tag)
create_payload=$(REL_TAG="$TAG" REL_NAME="$NAME" REL_BODY_TXT="$BODY" python3 - <<'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}')
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_resp=$(curl -sS -X POST \ create_resp=$(curl -sS -X POST \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
@@ -87,35 +192,13 @@ PY
"${API_URL}/repos/${OWNER}/${REPO}/releases" || true) "${API_URL}/repos/${OWNER}/${REPO}/releases" || true)
# Extract id from create response # Extract id from create response
rel_id=$(python3 - <<'PY' rel_id=$(echo "$create_resp" | jq -r '.id // empty')
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")
fi fi
# Fallback: search releases list for matching tag if still empty # Fallback: search releases list for matching tag if still empty
if [ -z "$rel_id" ]; then if [ -z "$rel_id" ]; then
list_resp=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases?limit=100") list_resp=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases?limit=100")
rel_id=$(python3 - <<'PY' rel_id=$(echo "$list_resp" | jq -r --arg tag "$TAG" '[.[] | select(.tag_name==$tag)][0].id // empty')
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")
fi fi
if [ -z "$rel_id" ]; then if [ -z "$rel_id" ]; then
@@ -124,37 +207,18 @@ if [ -z "$rel_id" ]; then
exit 1 exit 1
fi 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") assets_json=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets")
asset_id=$(python3 - <<'PY' asset_exists=$(echo "$assets_json" | jq -r --arg name "$ASSET_NAME" 'any(.[]; .name==$name)')
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")
if [ -n "$asset_id" ]; then if [ "$asset_exists" = "true" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets/${asset_id}" >/dev/null || true 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 fi
# Upload asset echo "Release ${TAG} ready (id=${rel_id})."
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}"

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"