diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7ac441..91accea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,8 +10,23 @@ on: 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: 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: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..44d8dc5 --- /dev/null +++ b/CHANGELOG.md @@ -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 `:` (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 `## - ` and add a new, empty `## Unreleased` above it. +2. In README and `docs/`, replace every marker line `*(unreleased)*` with `*(since )*`. +3. Raise the version in the note at the top of the README to `` (text and link). +4. Commit, then tag `` (without `v`, like `2.4.0`). + +The release workflow checks these steps and stops before publishing `:latest` if one is missing. diff --git a/README.md b/README.md index 91d6c54..57edeea 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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`. diff --git a/docs/environment_variables.md b/docs/environment_variables.md index daae937..a9694fc 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -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 @@ -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`: diff --git a/docs/installation.md b/docs/installation.md index 45fdd26..90cc576 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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/ diff --git a/release_docs_test.go b/release_docs_test.go new file mode 100644 index 0000000..46c01f8 --- /dev/null +++ b/release_docs_test.go @@ -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) + } + } + } +} diff --git a/scripts/check-release-docs.sh b/scripts/check-release-docs.sh new file mode 100755 index 0000000..858fdf6 --- /dev/null +++ b/scripts/check-release-docs.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Checks that README, docs and CHANGELOG are ready for release . +# Run by the release workflow before publishing; see "Releasing" in CHANGELOG.md. +set -u + +version="${1:?usage: $0 }" +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."