Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,23 @@
workflow_dispatch:

jobs:
docs:
name: Check docs for release
# A manual run on a branch has no version to check against.
if: github.ref_type == 'tag'
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Check README, docs and CHANGELOG
run: ./scripts/check-release-docs.sh "${{ github.ref_name }}"

docker:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
name: Publish Docker :release
needs: docs
# Publish only if the docs check passed, or did not run (manual run on a branch).
if: ${{ !cancelled() && (needs.docs.result == 'success' || needs.docs.result == 'skipped') }}
runs-on: ubuntu-latest

steps:
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Changelog

All notable changes are listed here. `Unreleased` is what `main` and the Docker image `:dev` contain; each version heading matches a release tag and the Docker image `:<version>` (the newest one is also `:latest`).

## Unreleased

### Breaking

- `/gen_keys` and `/remove_keys` accept `POST` only. `GET` now returns `405`. The dashboard already uses `POST`; scripts or bookmarks that call these endpoints with `GET` must switch. ([#170](https://github.com/wimaha/TeslaBleHttpProxy/pull/170))

### Security

- State-changing requests a browser sends from another website are rejected with `403`, so a page you visit can no longer change your keys or send commands to the car. evcc, curl and Home Assistant send neither `Origin` nor `Sec-Fetch-Site` and are not affected. ([#170](https://github.com/wimaha/TeslaBleHttpProxy/pull/170))
- Dashboard and log viewer escape all output (`html/template`). ([#170](https://github.com/wimaha/TeslaBleHttpProxy/pull/170))

### Added

- `trustedOrigins` environment variable for reverse proxies over plain HTTP that rewrite the `Host` header, and for web pages on other origins that call the API. See [environment variables](docs/environment_variables.md#trustedorigins). ([#170](https://github.com/wimaha/TeslaBleHttpProxy/pull/170))
- Docker image `:dev`, built from `main` on every push. ([#171](https://github.com/wimaha/TeslaBleHttpProxy/pull/171))
- README examples for `set_charge_limit` and `set_sentry_mode`. ([#169](https://github.com/wimaha/TeslaBleHttpProxy/pull/169))

### Fixed

- A client that disconnects while a response is written no longer stops the proxy. ([#170](https://github.com/wimaha/TeslaBleHttpProxy/pull/170))

### Changed

- Building from source requires Go 1.26 or newer. ([#172](https://github.com/wimaha/TeslaBleHttpProxy/pull/172))
- Tests run against a simulated vehicle, so commands, connection handling and the HTTP API are covered without a car. ([#169](https://github.com/wimaha/TeslaBleHttpProxy/pull/169))

## 2.4.0 - 2026-10-17

Changes up to and including 2.4.0 are described in the [GitHub releases](https://github.com/wimaha/TeslaBleHttpProxy/releases).

## Releasing

Before you push a release tag:

1. Rename `## Unreleased` to `## <version> - <date>` and add a new, empty `## Unreleased` above it.
2. In README and `docs/`, replace every marker line `*(unreleased)*` with `*(since <version>)*`.
3. Raise the version in the note at the top of the README to `<version>` (text and link).
4. Commit, then tag `<version>` (without `v`, like `2.4.0`).

The release workflow checks these steps and stops before publishing `:latest` if one is missing.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# TeslaBleHttpProxy

> **Note:** This README describes the current development state (`main`, Docker `:dev`). For the latest release **2.4.0** (Docker `:latest`) see the [README of 2.4.0](https://github.com/wimaha/TeslaBleHttpProxy/blob/2.4.0/README.md). What changed since then is listed in the [CHANGELOG](CHANGELOG.md). Sections marked *(unreleased)* are only in `:dev`.

TeslaBleHttpProxy is a program written in Go that receives HTTP requests and forwards them via Bluetooth to a Tesla vehicle. The program can, for example, be easily used together with [evcc](https://github.com/evcc-io/evcc).

The program stores the received requests in a queue and processes them one by one. This ensures that only one Bluetooth connection to the vehicle is established at a time.
Expand Down Expand Up @@ -287,6 +289,8 @@ This is a Tesla vehicle constraint, not a limitation of TeslaBleHttpProxy.

### Dashboard buttons return 403 Forbidden

*(unreleased)*

The proxy rejects state-changing requests that a browser sends from another website, so a page you visit cannot change your keys or send commands to the car. If you open the dashboard through a reverse proxy over plain HTTP that rewrites the `Host` header, your own requests can look cross-site too. Set the [`trustedOrigins`](docs/environment_variables.md#trustedorigins) environment variable to the address you open in the browser, e.g. `trustedOrigins=http://nas.local:8080`. Over HTTPS this is not needed.

The same applies to custom web pages or browser tools that call the API with `POST` from another origin: they get `403` unless their origin is listed in `trustedOrigins`.
Expand Down
4 changes: 4 additions & 0 deletions docs/environment_variables.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Environment variables

> **Note:** This page describes the current development state (`main`, Docker `:dev`). Sections marked *(unreleased)* are not in the latest release yet, see the [CHANGELOG](../CHANGELOG.md).

You can optionally set environment variables to override the default behavior.

## logLevel
Expand All @@ -24,6 +26,8 @@ This is the address and port to listen for HTTP requests. (Default: :8080)

## trustedOrigins

*(unreleased)*

Comma-separated list of origins (`scheme://host[:port]`) that may send state-changing requests (dashboard buttons, commands) from a browser. Requests from evcc, curl or Home Assistant integrations are not affected. (Default: empty)

You need it in two cases, because the proxy otherwise answers `403 Forbidden`:
Expand Down
2 changes: 2 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Installation

> **Note:** This page describes the current development state (`main`, Docker `:dev`). Sections marked *(unreleased)* are not in the latest release yet, see the [CHANGELOG](../CHANGELOG.md).

## Step 1: Install Raspberry Pi OS

Setup your raspberry Pi. You will find an installer here: https://www.raspberrypi.com/software/
Expand Down
70 changes: 70 additions & 0 deletions release_docs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"os"
"regexp"
"strings"
"testing"
)

// The README and docs describe main; these checks keep the pointers to the latest release
// consistent between releases. scripts/check-release-docs.sh does the stricter check on a tag.

func readFile(t *testing.T, name string) string {
t.Helper()
data, err := os.ReadFile(name)
if err != nil {
t.Fatal(err)
}
return string(data)
}

// latestRelease returns the first version section after "## Unreleased" in CHANGELOG.md.
func latestRelease(t *testing.T) string {
t.Helper()
var headings []string
for _, line := range strings.Split(readFile(t, "CHANGELOG.md"), "\n") {
if heading, ok := strings.CutPrefix(line, "## "); ok {
headings = append(headings, heading)
}
}
if len(headings) < 2 || headings[0] != "Unreleased" {
t.Fatalf("CHANGELOG.md must start with \"## Unreleased\" followed by the latest release, got %q", headings)
}
version, _, _ := strings.Cut(headings[1], " ")
return version
}

func TestReadmeNotePointsToLatestRelease(t *testing.T) {
version := latestRelease(t)
head := strings.Join(strings.SplitN(readFile(t, "README.md"), "\n", 6)[:5], "\n")
for _, want := range []string{"**" + version + "**", "/blob/" + version + "/README.md", "(CHANGELOG.md)"} {
if !strings.Contains(head, want) {
t.Errorf("the note at the top of README.md must contain %q (latest release in CHANGELOG.md is %s)", want, version)
}
}
}

func TestDocsHaveDevelopmentNote(t *testing.T) {
for _, name := range []string{"docs/environment_variables.md", "docs/installation.md"} {
if !strings.Contains(readFile(t, name), "(../CHANGELOG.md)") {
t.Errorf("%s needs the note that it describes main, with a link to the CHANGELOG", name)
}
}
}

// Markers must be a line of their own, so the release check finds them and heading anchors
// (like docs/environment_variables.md#trustedorigins) stay stable across releases.
func TestReleaseMarkersAreOwnLines(t *testing.T) {
marker := regexp.MustCompile(`(?i)\*\((unreleased|since [0-9.]+)\)\*`)
for _, name := range []string{"README.md", "docs/environment_variables.md", "docs/installation.md"} {
for i, line := range strings.Split(readFile(t, name), "\n") {
if strings.HasPrefix(line, "> **Note:**") || !marker.MatchString(line) {
continue
}
if trimmed := strings.TrimSpace(line); trimmed != "*(unreleased)*" && !regexp.MustCompile(`^\*\(since [0-9.]+\)\*$`).MatchString(trimmed) {
t.Errorf("%s:%d: put the marker on a line of its own, below the heading: %q", name, i+1, line)
}
}
}
}
30 changes: 30 additions & 0 deletions scripts/check-release-docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/sh
# Checks that README, docs and CHANGELOG are ready for release <version>.
# Run by the release workflow before publishing; see "Releasing" in CHANGELOG.md.
set -u

version="${1:?usage: $0 <version>}"
failed=0

markers=$(grep -n -x '\*(unreleased)\*' README.md docs/*.md)
if [ -n "$markers" ]; then
echo "Replace these markers with *(since $version)*:"
echo "$markers"
failed=1
fi

if ! grep -q -E "^## $(printf '%s' "$version" | sed 's/\./\\./g')( |$)" CHANGELOG.md; then
echo "CHANGELOG.md has no section '## $version'."
failed=1
fi

if ! head -n 5 README.md | grep -F "/blob/$version/README.md" | grep -q -F "**$version**"; then
echo "The note at the top of README.md does not point to release $version."
failed=1
fi

if [ "$failed" -ne 0 ]; then
echo "Docs are not ready for release $version."
exit 1
fi
echo "Docs are ready for release $version."
Loading