diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b13de45e6..bac484e185 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -212,6 +212,9 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] + outputs: + image: ${{ steps.vars.outputs.image }} + digest: ${{ steps.push_dev.outputs.digest || steps.push_tag.outputs.digest }} steps: - uses: actions/checkout@v6 @@ -220,9 +223,12 @@ jobs: uses: reviewdog/action-hadolint@v1 with: github_token: ${{ secrets.github_token }} - hadolint_ignore: DL3013 DL3008 + # DL3006: homeassistant/Dockerfile.HA's ARG BUILD_FROM is intentionally + # unpinned — CI always supplies an exact digest, see the ha-build job. + hadolint_ignore: DL3013 DL3008 DL3006 - name: Set Environment Variables + id: vars run: | OWNER="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]' | tr -d '-')" IMG=octobot @@ -236,6 +242,7 @@ jobs: echo "CONTAINER_NAME=octobot" >> $GITHUB_ENV echo "CHECK_TENTACLE_CONTAINER_TIME=10" >> $GITHUB_ENV echo "WAIT_CONTAINER_TIME=80" >> $GITHUB_ENV + echo "image=${OWNER}/${IMG}" >> $GITHUB_OUTPUT - name: Set up Python ${{ matrix.version }} uses: actions/setup-python@v6 @@ -327,7 +334,12 @@ jobs: docker logs ${{ env.CONTAINER_NAME }} docker inspect ${{ env.CONTAINER_NAME }} | jq '.[].State.Health.Status' | grep "healthy" + - name: Smoke test HA add-on + if: github.event_name != 'push' + run: homeassistant/tests/smoke.sh "${{ env.IMAGE }}:${{ env.SHA }}" + - name: Build and push latest + id: push_dev if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags') && github.ref == 'refs/heads/dev' uses: docker/build-push-action@v6 with: @@ -356,6 +368,7 @@ jobs: cache-to: type=local,dest=/tmp/.buildx-cache - name: Build and push on tag + id: push_tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: docker/build-push-action@v6 with: @@ -370,7 +383,167 @@ jobs: ${{ env.IMAGE }}:${{ env.VERSION }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache - + + ha-lint: + name: HA add-on lint - ${{ matrix.addon }} + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + addon: [ octobot, octobot-latest ] + steps: + - uses: actions/checkout@v6 + + - name: Lint add-on + uses: frenck/action-addon-linter@v2.21 + with: + path: "./homeassistant/${{ matrix.addon }}" + + - name: Shellcheck entrypoint + run: shellcheck homeassistant/rootfs/entrypoint.sh + + ha-prepare: + name: HA add-on - prepare + needs: [ docker ] + if: >- + github.event_name == 'push' && + (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + addon: ${{ steps.meta.outputs.addon }} + version: ${{ steps.meta.outputs.version }} + image_name: ${{ steps.meta.outputs.image_name }} + registry_prefix: ${{ steps.meta.outputs.registry_prefix }} + architectures: ${{ steps.meta.outputs.architectures }} + name: ${{ steps.meta.outputs.name }} + description: ${{ steps.meta.outputs.description }} + build_matrix: ${{ steps.matrix.outputs.matrix }} + build_from: ${{ needs.docker.outputs.image }}@${{ needs.docker.outputs.digest }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Guard - core digest must exist + env: + DIGEST: ${{ needs.docker.outputs.digest }} + run: | + [[ "$DIGEST" == sha256:* ]] || { echo "::error::docker job produced no digest"; exit 1; } + + - name: Compute add-on metadata + id: meta + env: + REF: ${{ github.ref }} + run: homeassistant/ci/addon-meta.sh + + - name: Prepare multi-arch matrix + id: matrix + uses: home-assistant/builder/actions/prepare-multi-arch-matrix@2026.06.0 + with: + architectures: ${{ steps.meta.outputs.architectures }} + image-name: ${{ steps.meta.outputs.image_name }} + registry-prefix: ${{ steps.meta.outputs.registry_prefix }} + + ha-build: + name: HA add-on - build ${{ matrix.arch }} + needs: [ ha-prepare ] + runs-on: ${{ matrix.os }} + permissions: + contents: read + packages: write + id-token: write + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.ha-prepare.outputs.build_matrix) }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + # BUILD_FROM is on Docker Hub and build-image sets pull:true, so every arch of + # every run pulls it anonymously. build-image's own ghcr.io login merges into + # ~/.docker/config.json, it does not replace this one. + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build add-on image + uses: home-assistant/builder/actions/build-image@2026.06.0 + with: + arch: ${{ matrix.arch }} + image: ${{ matrix.image }} + version: ${{ needs.ha-prepare.outputs.version }} + container-registry-password: ${{ secrets.GITHUB_TOKEN }} + context: ./homeassistant + file: ./homeassistant/Dockerfile.HA + build-args: | + BUILD_FROM=${{ needs.ha-prepare.outputs.build_from }} + cache-gha-scope: ${{ needs.ha-prepare.outputs.addon }} + image-tags: | + ${{ needs.ha-prepare.outputs.version }} + latest + labels: | + io.hass.type=app + io.hass.name=${{ needs.ha-prepare.outputs.name }} + io.hass.description=${{ needs.ha-prepare.outputs.description }} + org.opencontainers.image.title=Home Assistant Add-on: ${{ needs.ha-prepare.outputs.name }} + push: true + + ha-publish: + name: HA add-on - publish manifest & bump version + needs: [ ha-prepare, ha-build ] + runs-on: ubuntu-latest + # Only octobot (tag builds) auto-bumps config.yaml - see the step below. This group + # still matters: it serializes those (infrequent) tag-triggered bumps against each + # other so bump-version.sh's fetch/reset/push can't race itself; the script's own + # retry loop still guards against an unrelated human push landing on master in between. + concurrency: + group: ha-addon-publish + cancel-in-progress: false + permissions: + contents: write + packages: write + id-token: write + steps: + # Runs for both channels: the image itself always stays fresh and digest-pinned + # to the exact base image, regardless of whether Supervisor's version bumps. + - name: Publish multi-arch manifest + uses: home-assistant/builder/actions/publish-multi-arch-manifest@2026.06.0 + with: + architectures: ${{ needs.ha-prepare.outputs.architectures }} + image-name: ${{ needs.ha-prepare.outputs.image_name }} + registry-prefix: ${{ needs.ha-prepare.outputs.registry_prefix }} + container-registry-password: ${{ secrets.GITHUB_TOKEN }} + image-tags: | + ${{ needs.ha-prepare.outputs.version }} + latest + + # octobot-latest (DEV) is a deliberate manual bump, not automated: see + # homeassistant/octobot-latest/config.yaml. Only octobot (a tag push, i.e. a real + # OctoBot release) auto-advances the version Supervisor offers as an update. + # + # Supervisor clones the default branch (master) regardless of which branch or tag + # triggered this run - see supervisor/store/git.py::_clone - so the bump always + # targets master explicitly, not github.ref (which is detached for a tag build). + - uses: actions/checkout@v6 + if: needs.ha-prepare.outputs.addon == 'octobot' + with: + ref: master + fetch-depth: 0 + + - name: Bump add-on version on master + if: needs.ha-prepare.outputs.addon == 'octobot' + env: + ADDON: ${{ needs.ha-prepare.outputs.addon }} + NEW_VERSION: ${{ needs.ha-prepare.outputs.version }} + run: homeassistant/ci/bump-version.sh + tentacles: needs: [ build, tests ] if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/') @@ -790,6 +963,10 @@ jobs: - version - docs - publish-npm + - ha-lint + - ha-prepare + - ha-build + - ha-publish uses: Drakkar-Software/.github/.github/workflows/failure_notify_workflow.yml@master secrets: DISCORD_GITHUB_WEBHOOK: ${{ secrets.DISCORD_GITHUB_WEBHOOK }} diff --git a/homeassistant/Dockerfile.HA b/homeassistant/Dockerfile.HA new file mode 100644 index 0000000000..9bc175e7b9 --- /dev/null +++ b/homeassistant/Dockerfile.HA @@ -0,0 +1,7 @@ +ARG BUILD_FROM +FROM ${BUILD_FROM} + +COPY rootfs / +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/homeassistant/ci/addon-meta.sh b/homeassistant/ci/addon-meta.sh new file mode 100755 index 0000000000..ebeb242202 --- /dev/null +++ b/homeassistant/ci/addon-meta.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Computes which add-on channel to build and what version to publish it under. +# +# Deliberately NOT using home-assistant/actions/helpers/info@master: that action +# reads `version:` from the committed config.yaml. For octobot that field only ever +# holds a "0.0.0" placeholder here — bump-version.sh stamps the real value afterwards, +# from this script's `version` output, on every tag push. For octobot-latest `version` +# here is only the Docker image tag (traceability) — config.yaml's version is a +# deliberate manual bump instead, see homeassistant/octobot-latest/config.yaml. +# +# Inputs (env): REF (github.ref) +# Output: appends addon/version/image_name/registry_prefix/architectures/name/description +# to $GITHUB_OUTPUT (or stdout, when run locally). +set -euo pipefail + +out="${GITHUB_OUTPUT:-/dev/stdout}" +here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # homeassistant/ + +if [[ "$REF" == refs/tags/* ]]; then + addon="octobot" + version="${REF##refs/tags/}" +else + addon="octobot-latest" + base="$(grep -Po '^VERSION\s*=\s*"\K[^"]+' "$here/../octobot/__init__.py")" + base="${base%%-*}"; base="${base%%+*}" # 3.0.0-beta1 -> 3.0.0 + # Date-based, not a run counter: monotonic by construction, no dependency on + # github.run_number (which is per-workflow-*file* and silently resets if + # main.yml is ever renamed/recreated, which would make a rebuild look older + # than the last real one). + version="${base}-dev.$(date -u +%Y%m%d%H%M)" +fi + +# Docker tag charset guard — catch an illegal tag (e.g. '+') before spending a +# 20-minute build. AwesomeVersion also ignores SemVer build metadata, so a +# scheme using '+' would silently never be offered as an update either way. +if [[ ! "$version" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$ ]]; then + echo "::error::'$version' is not a valid Docker tag" >&2 + exit 1 +fi + +cfg="$here/${addon}/config.yaml" +image="$(yq -r '.image' "$cfg")" + +{ + echo "addon=${addon}" + echo "version=${version}" + echo "image_name=${image##*/}" + echo "registry_prefix=${image%/*}" + echo "architectures=$(yq -o=json -I=0 '.arch' "$cfg")" # matrix action needs a JSON array + echo "name=$(yq -r '.name' "$cfg")" + echo "description=$(yq -r '.description' "$cfg")" +} >> "$out" diff --git a/homeassistant/ci/bump-version.sh b/homeassistant/ci/bump-version.sh new file mode 100755 index 0000000000..b8e656fde6 --- /dev/null +++ b/homeassistant/ci/bump-version.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Stamps the just-published version into homeassistant//config.yaml and +# pushes it directly to master — the branch Supervisor actually clones (see +# supervisor/store/git.py::_clone, which omits the branch arg entirely unless +# the user's repository URL has a '#branch' suffix, so it defaults to the repo's +# default branch). +# +# Only ever invoked for ADDON=octobot (a tag push, i.e. a real OctoBot release) — +# see the `if:` guard on this step in ha-publish. octobot-latest's version is a +# deliberate manual bump instead; see homeassistant/octobot-latest/config.yaml. +# +# Runs from a checkout of THIS repo (core OctoBot repo doubles as the add-on +# distribution repo per the user's decision to accept the larger Supervisor +# clone rather than maintain a separate thin repo). +# +# Inputs (env): ADDON, NEW_VERSION +set -euo pipefail + +# Assumes CWD is the repo root (true for a plain `actions/checkout@v6` step with +# no working-directory override), same as the git commands below. +cfg_rel="homeassistant/${ADDON}/config.yaml" + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +# Retry loop: a concurrency group (see ha-publish) already serializes concurrent +# runs of this step against each other, but an unrelated human push to master in +# between fetch and push is still possible - retry rather than fail the run. +for attempt in 1 2 3 4 5; do + git fetch origin master + git reset --hard origin/master + + # Explicit double-quote style: a bare 1.10 would otherwise parse as a float. + V="$NEW_VERSION" yq -i '.version = strenv(V) | .version style="double"' "$cfg_rel" + + if git diff --quiet -- "$cfg_rel"; then + echo "already at ${NEW_VERSION}, nothing to commit" + exit 0 + fi + + git add "$cfg_rel" + git commit -m "ci(ha): bump ${ADDON} add-on to ${NEW_VERSION} [skip ci]" + if git push origin HEAD:master; then + exit 0 + fi + echo "push race on attempt ${attempt}, retrying" + sleep $((attempt * 5)) +done + +echo "::error::could not push ${ADDON} version bump to master" +exit 1 diff --git a/homeassistant/octobot-latest/CHANGELOG.md b/homeassistant/octobot-latest/CHANGELOG.md new file mode 100644 index 0000000000..34df224a47 --- /dev/null +++ b/homeassistant/octobot-latest/CHANGELOG.md @@ -0,0 +1,25 @@ + + + +## 1.1.2 + +- Add optional `node_external_host` configuration option: sets the external host + (and port, if non-default) the Node sync/mobile features advertise, for + reverse-proxy / public-hostname setups. Leave empty to keep OctoBot's default. + +## 1.1.1 + +- Rebuild against the current OctoBot base image. + +## 1.1.0 + +- Tentacles are no longer persisted: they are wiped on every start so OctoBot + reinstalls a fresh default tentacles set each boot. User config, logs and + backtesting data still persist under `/data`. + +## 1.0.0 + +- Initial release: wraps `drakkarsoftware/octobot:latest`, exposes the Node + web interface on port 8000 with a web UI button, persists OctoBot data + under `/data`. diff --git a/homeassistant/octobot-latest/DOCS.md b/homeassistant/octobot-latest/DOCS.md new file mode 100644 index 0000000000..f70164839f --- /dev/null +++ b/homeassistant/octobot-latest/DOCS.md @@ -0,0 +1,45 @@ +# Home Assistant Add-on: OctoBot DEV + +## How to use + +This add-on runs [OctoBot](https://github.com/Drakkar-Software/OctoBot), an +open-source cryptocurrency trading bot, from the official +`drakkarsoftware/octobot:latest` Docker image. + +Click **OPEN WEB UI** (or go to `http://:8000/app`) to reach the OctoBot +Node web interface. The web interface is served without a login by default; +configure exchanges, strategies and password protection from within OctoBot +itself once it's running. + +> The Node web interface is being rolled out as OctoBot's default UI. If +> `/app` returns a 404 on your installed OctoBot version, the node web bundle +> isn't baked into that build yet — uncomment the `5001/tcp` port in this +> add-on's config and point the web UI button at +> `http://:5001` (the classic dashboard) as a fallback. + +## Configuration + +### Option: `enable_node_api` + +Enables OctoBot's Node API/web interface service on port 8000. Leave this on +unless you only need the classic dashboard on 5001. + +### Option: `node_external_host` (optional) + +External host (and port, if non-default) the Node sync/mobile features advertise +— set this when OctoBot is reached through a reverse proxy or a public hostname +different from the add-on's own address. Leave empty to keep OctoBot's default. + +## Data persistence + +OctoBot's user config, logs and backtesting data are stored under this add-on's +persistent `/data` folder, so they survive add-on updates and rebuilds. + +Installed **tentacles are intentionally not persisted**: they are wiped on every +start, so OctoBot reinstalls a fresh default tentacles set each time the add-on +boots. + +## Want a stable release instead? + +Install the **OctoBot** add-on instead, which tracks +`drakkarsoftware/octobot:stable`. diff --git a/homeassistant/octobot-latest/README.md b/homeassistant/octobot-latest/README.md new file mode 100644 index 0000000000..d120c8bef5 --- /dev/null +++ b/homeassistant/octobot-latest/README.md @@ -0,0 +1,10 @@ +# Home Assistant Add-on: OctoBot DEV + +_Run [OctoBot](https://github.com/Drakkar-Software/OctoBot), an open-source +cryptocurrency trading bot, as a Home Assistant add-on (DEV/latest channel)._ + +![Supports aarch64 Architecture][aarch64-shield] +![Supports amd64 Architecture][amd64-shield] + +[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg +[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg diff --git a/homeassistant/octobot-latest/config.yaml b/homeassistant/octobot-latest/config.yaml new file mode 100644 index 0000000000..e31ceacc55 --- /dev/null +++ b/homeassistant/octobot-latest/config.yaml @@ -0,0 +1,33 @@ +# https://developers.home-assistant.io/docs/apps/configuration#app-configuration +name: OctoBot DEV +# Manually maintained, unlike homeassistant/octobot/config.yaml's version (which +# ha-publish auto-bumps on every tag push). The DEV image itself is still rebuilt and +# republished automatically on every push to dev (see the ha-build/ha-publish jobs) and +# always stays digest-pinned to the exact core :latest image that triggered it — only +# Supervisor's "update available" signal for already-installed users requires this field +# to change. Bump it by hand when you want existing DEV installs to see an update. +version: "1.1.5" +slug: octobot-latest +description: OctoBot crypto trading bot (DEV/latest) with Node web interface +url: "https://github.com/drakkar-software/OctoBot/tree/master/homeassistant/octobot-latest" +arch: + - aarch64 + - amd64 +init: false +ports: + 8000/tcp: 8000 + # 5001/tcp: 5001 # fallback: classic web dashboard, uncomment if node /app 404s +ports_description: + 8000/tcp: "OctoBot Node web interface" +webui: "http://[HOST]:[PORT:8000]/app" +options: + enable_node_api: true +schema: + enable_node_api: bool + node_external_host: str? +image: "ghcr.io/drakkar-software/addon-octobot-latest" +# ^ No {arch} placeholder: both the CI builder and the Supervisor prepend the +# arch prefix themselves (e.g. amd64-addon-octobot-latest); a literal {arch} +# token in this name breaks the CI build (invalid tag: double arch prefix). +# Required for CI to know where to publish; comment out only for local +# Supervisor builds from the Dockerfile — keep it set on main. diff --git a/homeassistant/octobot-latest/icon.png b/homeassistant/octobot-latest/icon.png new file mode 100644 index 0000000000..4b35088f76 Binary files /dev/null and b/homeassistant/octobot-latest/icon.png differ diff --git a/homeassistant/octobot-latest/logo.png b/homeassistant/octobot-latest/logo.png new file mode 100644 index 0000000000..5332bcd3ed Binary files /dev/null and b/homeassistant/octobot-latest/logo.png differ diff --git a/homeassistant/octobot-latest/translations/en.yaml b/homeassistant/octobot-latest/translations/en.yaml new file mode 100644 index 0000000000..f2183d7793 --- /dev/null +++ b/homeassistant/octobot-latest/translations/en.yaml @@ -0,0 +1,7 @@ +configuration: + enable_node_api: + name: Enable Node API + description: Serve the OctoBot Node web interface on port 8000 (opened by the web UI button). + node_external_host: + name: Node external host + description: Optional. External host (and port, if non-default) the Node sync/mobile features advertise, e.g. behind a reverse proxy or public hostname. Leave empty to use OctoBot's default. diff --git a/homeassistant/octobot/CHANGELOG.md b/homeassistant/octobot/CHANGELOG.md new file mode 100644 index 0000000000..a2d9ea33b9 --- /dev/null +++ b/homeassistant/octobot/CHANGELOG.md @@ -0,0 +1,25 @@ + + + +## 1.1.2 + +- Add optional `node_external_host` configuration option: sets the external host + (and port, if non-default) the Node sync/mobile features advertise, for + reverse-proxy / public-hostname setups. Leave empty to keep OctoBot's default. + +## 1.1.1 + +- Rebuild against the current OctoBot base image. + +## 1.1.0 + +- Tentacles are no longer persisted: they are wiped on every start so OctoBot + reinstalls a fresh default tentacles set each boot. User config, logs and + backtesting data still persist under `/data`. + +## 1.0.0 + +- Initial release: wraps `drakkarsoftware/octobot:stable`, exposes the Node + web interface on port 8000 with a web UI button, persists OctoBot data + under `/data`. diff --git a/homeassistant/octobot/DOCS.md b/homeassistant/octobot/DOCS.md new file mode 100644 index 0000000000..7b485d23de --- /dev/null +++ b/homeassistant/octobot/DOCS.md @@ -0,0 +1,45 @@ +# Home Assistant Add-on: OctoBot + +## How to use + +This add-on runs [OctoBot](https://github.com/Drakkar-Software/OctoBot), an +open-source cryptocurrency trading bot, from the official +`drakkarsoftware/octobot:stable` Docker image. + +Click **OPEN WEB UI** (or go to `http://:8000/app`) to reach the OctoBot +Node web interface. The web interface is served without a login by default; +configure exchanges, strategies and password protection from within OctoBot +itself once it's running. + +> The Node web interface is being rolled out as OctoBot's default UI. If +> `/app` returns a 404 on your installed OctoBot version, the node web bundle +> isn't baked into that build yet — uncomment the `5001/tcp` port in this +> add-on's config and point the web UI button at +> `http://:5001` (the classic dashboard) as a fallback. + +## Configuration + +### Option: `enable_node_api` + +Enables OctoBot's Node API/web interface service on port 8000. Leave this on +unless you only need the classic dashboard on 5001. + +### Option: `node_external_host` (optional) + +External host (and port, if non-default) the Node sync/mobile features advertise +— set this when OctoBot is reached through a reverse proxy or a public hostname +different from the add-on's own address. Leave empty to keep OctoBot's default. + +## Data persistence + +OctoBot's user config, logs and backtesting data are stored under this add-on's +persistent `/data` folder, so they survive add-on updates and rebuilds. + +Installed **tentacles are intentionally not persisted**: they are wiped on every +start, so OctoBot reinstalls a fresh default tentacles set each time the add-on +boots. + +## Want the bleeding edge instead? + +Install the **OctoBot DEV** add-on instead, which tracks +`drakkarsoftware/octobot:latest`. diff --git a/homeassistant/octobot/README.md b/homeassistant/octobot/README.md new file mode 100644 index 0000000000..fa59c097c6 --- /dev/null +++ b/homeassistant/octobot/README.md @@ -0,0 +1,10 @@ +# Home Assistant Add-on: OctoBot + +_Run [OctoBot](https://github.com/Drakkar-Software/OctoBot), an open-source +cryptocurrency trading bot, as a Home Assistant add-on (stable channel)._ + +![Supports aarch64 Architecture][aarch64-shield] +![Supports amd64 Architecture][amd64-shield] + +[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg +[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg diff --git a/homeassistant/octobot/config.yaml b/homeassistant/octobot/config.yaml new file mode 100644 index 0000000000..dd4bf7c027 --- /dev/null +++ b/homeassistant/octobot/config.yaml @@ -0,0 +1,30 @@ +# https://developers.home-assistant.io/docs/apps/configuration#app-configuration +name: OctoBot +# Placeholder only — never edit by hand. ha-publish overwrites this field on every +# tag push (see homeassistant/ci/bump-version.sh), so it always tracks the OctoBot +# git tag that triggered the release. +version: "0.0.0" +slug: octobot +description: OctoBot crypto trading bot (stable) with Node web interface +url: "https://github.com/drakkar-software/OctoBot/tree/master/homeassistant/octobot" +arch: + - aarch64 + - amd64 +init: false +ports: + 8000/tcp: 8000 + # 5001/tcp: 5001 # fallback: classic web dashboard, uncomment if node /app 404s +ports_description: + 8000/tcp: "OctoBot Node web interface" +webui: "http://[HOST]:[PORT:8000]/app" +options: + enable_node_api: true +schema: + enable_node_api: bool + node_external_host: str? +image: "ghcr.io/drakkar-software/addon-octobot" +# ^ No {arch} placeholder: both the CI builder and the Supervisor prepend the +# arch prefix themselves (e.g. amd64-addon-octobot); a literal {arch} token +# in this name breaks the CI build (invalid tag: double arch prefix). +# Required for CI to know where to publish; comment out only for local +# Supervisor builds from the Dockerfile — keep it set on main. diff --git a/homeassistant/octobot/icon.png b/homeassistant/octobot/icon.png new file mode 100644 index 0000000000..4b35088f76 Binary files /dev/null and b/homeassistant/octobot/icon.png differ diff --git a/homeassistant/octobot/logo.png b/homeassistant/octobot/logo.png new file mode 100644 index 0000000000..5332bcd3ed Binary files /dev/null and b/homeassistant/octobot/logo.png differ diff --git a/homeassistant/octobot/translations/en.yaml b/homeassistant/octobot/translations/en.yaml new file mode 100644 index 0000000000..f2183d7793 --- /dev/null +++ b/homeassistant/octobot/translations/en.yaml @@ -0,0 +1,7 @@ +configuration: + enable_node_api: + name: Enable Node API + description: Serve the OctoBot Node web interface on port 8000 (opened by the web UI button). + node_external_host: + name: Node external host + description: Optional. External host (and port, if non-default) the Node sync/mobile features advertise, e.g. behind a reverse proxy or public hostname. Leave empty to use OctoBot's default. diff --git a/homeassistant/rootfs/entrypoint.sh b/homeassistant/rootfs/entrypoint.sh new file mode 100755 index 0000000000..88f6556760 --- /dev/null +++ b/homeassistant/rootfs/entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +OPTS=/data/options.json +enable_node_api() { python3 -c "import json;print(str(json.load(open('$OPTS')).get('enable_node_api',True)).lower())" 2>/dev/null || echo true; } +node_external_host() { python3 -c "import json;print(json.load(open('$OPTS')).get('node_external_host') or '')" 2>/dev/null || echo ""; } + +export ENABLE_NODE_API="$(enable_node_api)" +export AUTO_OPEN_IN_WEB_BROWSER=false + +# Optional external host (and port, if non-default) the Node sync/mobile features +# advertise — set it when OctoBot sits behind a reverse proxy or a public +# hostname. Only exported when non-empty so OctoBot keeps its own default. +NODE_EXTERNAL_HOST_VALUE="$(node_external_host)" +[ -n "$NODE_EXTERNAL_HOST_VALUE" ] && export NODE_EXTERNAL_HOST="$NODE_EXTERNAL_HOST_VALUE" + +# OctoBot resolves its user/tentacles/backtesting/logs folders as plain relative +# paths against the process's current working directory (no env override exists +# for user/tentacles/backtesting). Relocating CWD to HA's persistent /data keeps +# them off the base image's inherited /octobot/{user,tentacles,logs,backtesting} +# VOLUMEs, so data survives add-on rebuilds/updates. +cd /data + +# Tentacles are intentionally NOT persisted: wipe them on every start so OctoBot +# reinstalls a fresh default tentacles set each boot. user/logs/backtesting stay +# under /data and keep persisting. +rm -rf /data/tentacles + +exec OctoBot "$@" diff --git a/homeassistant/tests/options.json b/homeassistant/tests/options.json new file mode 100644 index 0000000000..b058d06a3d --- /dev/null +++ b/homeassistant/tests/options.json @@ -0,0 +1,3 @@ +{ + "enable_node_api": true +} diff --git a/homeassistant/tests/smoke.sh b/homeassistant/tests/smoke.sh new file mode 100755 index 0000000000..6d1e1af980 --- /dev/null +++ b/homeassistant/tests/smoke.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Builds homeassistant/Dockerfile.HA on top of a given core OctoBot image and +# asserts the behaviours rootfs/entrypoint.sh exists to provide — the entire +# reason the HA wrapper exists over running the base image directly: +# +# 1. the container answers on :8000/app (Node web interface) +# 2. /data/tentacles is wiped on every boot (tentacles are never persisted) +# 3. the running process's cwd is /data (so user/logs/backtesting persist +# under HA's /data volume, not the base image's /octobot/* VOLUMEs) +# 4. ENABLE_NODE_API reflects options.json, and NODE_EXTERNAL_HOST stays +# unset when the option is absent/empty +# +# Usage: homeassistant/tests/smoke.sh +# No registry access required if is already loaded locally +# (docker/build-push-action's `load: true` in the docker job's PR path does this). +set -euo pipefail + +BASE_FROM="${1:?usage: smoke.sh }" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # homeassistant/ +IMG="octobot-ha-smoketest" +CONTAINER="octobot-ha-smoketest" +DATA_DIR="$(mktemp -d)" +FAIL=0 + +cleanup() { + docker rm -f "$CONTAINER" >/dev/null 2>&1 || true + rm -rf "$DATA_DIR" +} +trap cleanup EXIT + +check() { + local desc="$1" got="$2" want="$3" + if [[ "$got" == "$want" ]]; then + echo "OK - $desc" + else + echo "FAIL - $desc (got: '$got', want: '$want')" + FAIL=1 + fi +} + +echo "Building $IMG from BUILD_FROM=$BASE_FROM ..." +docker build -t "$IMG" --build-arg "BUILD_FROM=$BASE_FROM" -f "$HERE/Dockerfile.HA" "$HERE" + +mkdir -p "$DATA_DIR/tentacles" +touch "$DATA_DIR/tentacles/marker" # must be gone after boot: tentacles are wiped every start +cp "$HERE/tests/options.json" "$DATA_DIR/options.json" + +echo "Starting $CONTAINER ..." +docker run -d --name "$CONTAINER" -p 18000:8000 -v "$DATA_DIR:/data" "$IMG" >/dev/null + +# The wrapper contract (cwd, tentacle wipe, env translation) is set up by +# rootfs/entrypoint.sh before it execs OctoBot, so these don't need to wait for +# the app to finish booting — just for the container to be up. entrypoint execs +# OctoBot as the final step, so OctoBot is PID 1 inside the container. +sleep 5 + +CWD="$(docker exec "$CONTAINER" readlink /proc/1/cwd)" +check "process cwd is /data" "$CWD" "/data" + +check "/data/tentacles wiped on boot" "$( [[ -e "$DATA_DIR/tentacles/marker" ]] && echo present || echo absent )" "absent" + +ENVIRON="$(docker exec "$CONTAINER" sh -c "tr '\0' '\n' < /proc/1/environ")" +check "ENABLE_NODE_API reflects options.json" "$(grep -c '^ENABLE_NODE_API=true$' <<<"$ENVIRON")" "1" +check "NODE_EXTERNAL_HOST unset when absent from options.json" "$(grep -c '^NODE_EXTERNAL_HOST=' <<<"$ENVIRON")" "0" + +# Non-fatal: OctoBot must download a full tentacles set from tentacles.octobot.online +# before /app serves anything (tentacles are wiped above, unlike the core docker job's +# health check which mounts tentacles from the host). Also documented in DOCS.md as +# occasionally 404-ing on builds that predate the node web bundle, with :5001 as the +# fallback. A slow or missing response here is not evidence the wrapper is broken. +echo "Waiting for :8000/app (best-effort, not a hard gate) ..." +up=false +for _ in $(seq 1 60); do + if curl -sf -o /dev/null "http://127.0.0.1:18000/app"; then + up=true + break + fi + sleep 3 +done +if [[ "$up" == "true" ]]; then + echo "OK - answers on :8000/app" +else + echo "WARN - never answered on :8000/app within 180s (non-fatal, see comment above)" + docker logs "$CONTAINER" || true +fi + +exit "$FAIL" diff --git a/packages/commons/octobot_commons/tests/test_config.py b/packages/commons/octobot_commons/tests/test_config.py index 2bd7c70289..1eae936711 100644 --- a/packages/commons/octobot_commons/tests/test_config.py +++ b/packages/commons/octobot_commons/tests/test_config.py @@ -29,7 +29,7 @@ def get_test_config(test_folder=TEST_FOLDER): Return test default config :return: test default config """ - return os.path.join(test_folder, STATIC_FOLDER, constants.CONFIG_FILE) + return os.path.join(test_folder, STATIC_FOLDER, "bot_config.json") def get_test_profile(test_folder=TEST_FOLDER): diff --git a/packages/commons/tests/static/config.json b/packages/commons/tests/static/bot_config.json similarity index 100% rename from packages/commons/tests/static/config.json rename to packages/commons/tests/static/bot_config.json diff --git a/packages/evaluators/tests/static/config.json b/packages/evaluators/tests/static/bot_config.json similarity index 100% rename from packages/evaluators/tests/static/config.json rename to packages/evaluators/tests/static/bot_config.json diff --git a/packages/tentacles/Meta/Keywords/scripting_library/tests/static/config.json b/packages/tentacles/Meta/Keywords/scripting_library/tests/static/bot_config.json similarity index 100% rename from packages/tentacles/Meta/Keywords/scripting_library/tests/static/config.json rename to packages/tentacles/Meta/Keywords/scripting_library/tests/static/bot_config.json diff --git a/packages/trading/tests/static/config.json b/packages/trading/tests/static/bot_config.json similarity index 100% rename from packages/trading/tests/static/config.json rename to packages/trading/tests/static/bot_config.json diff --git a/repository.yaml b/repository.yaml new file mode 100644 index 0000000000..f2f8da648b --- /dev/null +++ b/repository.yaml @@ -0,0 +1,3 @@ +name: OctoBot Home Assistant app repository +url: 'https://github.com/Drakkar-Software/OctoBot' +maintainer: 'Drakkar-Software ' diff --git a/tests/static/config.json b/tests/static/bot_config.json similarity index 100% rename from tests/static/config.json rename to tests/static/bot_config.json diff --git a/tests/unit_tests/test_configuration_manager.py b/tests/unit_tests/test_configuration_manager.py index 6edad45f00..748b311bbc 100644 --- a/tests/unit_tests/test_configuration_manager.py +++ b/tests/unit_tests/test_configuration_manager.py @@ -34,7 +34,7 @@ def test_init_config(): configuration_manager.init_config( config_file=config_path, - from_config_file=os.path.join(test_config.TEST_CONFIG_FOLDER, commons_constants.CONFIG_FILE), + from_config_file=os.path.join(test_config.TEST_CONFIG_FOLDER, "bot_config.json"), ) assert os.path.isfile(config_path) os.remove(config_path) @@ -50,7 +50,7 @@ def test_init_config_uses_runtime_user_root_not_import_time_default(tmp_path): configuration_manager.init_config( from_config_file=os.path.join( test_config.TEST_CONFIG_FOLDER, - commons_constants.CONFIG_FILE, + "bot_config.json", ), ) assert automation_config_path.is_file()