From c3bd4c2cef259ccf57f42218929ba7aba24f5192 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 20 Aug 2026 17:32:06 -0700 Subject: [PATCH 1/3] CI/CD improvements --- .github/dependabot.yml | 34 ++++++++++ .github/workflows/ci.yml | 59 +++++++++++++++++ .github/workflows/labels.yml | 82 +++++++++++++++++++++++ .github/workflows/release.yml | 115 +++++++++++++++++++++++++++++++++ LegoTrain/Dockerfile | 2 +- LegoTrain/Dockerfile.arm32 | 2 +- LegoTrain/Dockerfile.arm64 | 2 +- README.md | 41 +++++++++++- TrainDetect/Models/Detector.cs | 2 +- 9 files changed, 332 insertions(+), 7 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/labels.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5b94d23 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,34 @@ +version: 2 +updates: + - package-ecosystem: nuget + directory: / + schedule: + interval: weekly + labels: + - dependency update + groups: + nuget-dependencies: + patterns: + - "*" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + labels: + - dependency update + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: docker + directory: /LegoTrain + schedule: + interval: weekly + labels: + - dependency update + groups: + docker-images: + patterns: + - "*" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..541e98f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + dotnet: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + - name: Test discovery messages + run: dotnet test DiscoveryMessageTests/DiscoveryMessageTests.csproj --configuration Release + - name: Build LegoTrain + run: dotnet build LegoTrain/LegoTrain.csproj --configuration Release + + firmware: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: NuGet/setup-nuget@v2 + - uses: nanoframework/nanobuild@v1 + with: + gitHubAuth: ${{ github.token }} + - name: Restore nanoFramework packages + run: nuget restore LegoTrain.sln -NonInteractive + - name: Build nanoFramework projects + shell: pwsh + run: | + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 + if (-not $msbuild) { throw 'MSBuild was not found.' } + $projects = 'LegoInfrared', 'SignalSwitch', 'TrainDetect' + foreach ($project in $projects) { + & $msbuild "$project/$project.nfproj" /t:Rebuild /p:Configuration=Release /p:Platform=AnyCPU /m + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: LegoTrain/Dockerfile + push: false \ No newline at end of file diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml new file mode 100644 index 0000000..dbaec43 --- /dev/null +++ b/.github/workflows/labels.yml @@ -0,0 +1,82 @@ +name: Labels + +on: + push: + branches: + - main + pull_request_target: + types: + - opened + - reopened + - synchronize + workflow_dispatch: + +permissions: + contents: read + issues: write + pull-requests: read + +jobs: + labels: + runs-on: ubuntu-latest + steps: + - name: Create labels and label pull request + uses: actions/github-script@v7 + with: + script: | + const definitions = { + LegoTrain: ['LegoTrain/', 'nanoDiscovery/', 'LegoDiscoveryCommon/'], + LegoInfrared: ['LegoInfrared/', 'SharedServices/', 'nanoDiscovery/', 'LegoDiscoveryCommon/'], + SignalSwitch: ['SignalSwitch/', 'SharedServices/', 'nanoDiscovery/', 'LegoDiscoveryCommon/'], + TrainDetect: ['TrainDetect/', 'SharedServices/', 'nanoDiscovery/', 'LegoDiscoveryCommon/'], + DiscoveryMessageTests: ['DiscoveryMessageTests/', 'nanoDiscovery/'], + documentation: ['README.md', 'Lego_api_doc.md', 'LICENSE'], + cicd: ['.github/', 'build-docker.ps1', 'build-docker.sh'], + }; + const colors = { + LegoTrain: '1f6feb', + LegoInfrared: 'd73a4a', + SignalSwitch: 'fbca04', + TrainDetect: '0e8a16', + DiscoveryMessageTests: '5319e7', + documentation: '0075ca', + cicd: '6f42c1', + 'dependency update': '0366d6', + }; + + for (const [name, color] of Object.entries(colors)) { + try { + await github.rest.issues.getLabel({ ...context.repo, name }); + } catch (error) { + if (error.status !== 404) throw error; + await github.rest.issues.createLabel({ ...context.repo, name, color }); + } + } + + if (!context.payload.pull_request) return; + + const files = await github.paginate(github.rest.pulls.listFiles, { + ...context.repo, + pull_number: context.issue.number, + per_page: 100, + }); + const paths = files.map(file => file.filename); + const labels = Object.entries(definitions) + .filter(([, prefixes]) => prefixes.some(prefix => + paths.some(path => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix))) + .map(([name]) => name); + + const dependencyFiles = paths.some(path => + /(^|\/)(packages\.config|dependabot\.yml)$/.test(path) || + /\.(csproj|nfproj)$/.test(path)); + if (context.payload.pull_request.user.login === 'dependabot[bot]' || dependencyFiles) { + labels.push('dependency update'); + } + + if (labels.length > 0) { + await github.rest.issues.addLabels({ + ...context.repo, + issue_number: context.issue.number, + labels, + }); + } \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8e4dcba --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,115 @@ +name: Release + +on: + push: + tags: + - publish + - publish-* + release: + types: + - published + +permissions: + contents: write + packages: write + +jobs: + firmware: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: NuGet/setup-nuget@v2 + - uses: nanoframework/nanobuild@v1 + with: + gitHubAuth: ${{ github.token }} + - name: Restore nanoFramework packages + run: nuget restore LegoTrain.sln -NonInteractive + - name: Build firmware + shell: pwsh + run: | + New-Item -ItemType Directory -Path firmware + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 + if (-not $msbuild) { throw 'MSBuild was not found.' } + $projects = 'LegoInfrared', 'SignalSwitch', 'TrainDetect' + foreach ($project in $projects) { + & $msbuild "$project/$project.nfproj" /t:Rebuild /p:Configuration=Release /p:Platform=AnyCPU /m + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Copy-Item "$project/bin/Release/$project.bin" "firmware/$project.bin" + } + - uses: actions/upload-artifact@v4 + with: + name: firmware + path: firmware/*.bin + if-no-files-found: error + + docker: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + platform: linux/amd64 + dockerfile: LegoTrain/Dockerfile + - architecture: arm32 + platform: linux/arm/v7 + dockerfile: LegoTrain/Dockerfile.arm32 + - architecture: arm64 + platform: linux/arm64 + dockerfile: LegoTrain/Dockerfile.arm64 + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Resolve image metadata + id: metadata + shell: bash + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + version="${{ github.event.release.tag_name }}" + else + version="${GITHUB_REF_NAME}" + fi + echo "image=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Build and push ${{ matrix.architecture }} image + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: ${{ matrix.platform }} + push: true + tags: ${{ steps.metadata.outputs.image }}:${{ matrix.architecture }}-${{ steps.metadata.outputs.version }} + + github-release: + needs: + - firmware + - docker + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: firmware + path: firmware + - name: Resolve release tag + id: release + shell: bash + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT" + else + echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + fi + - name: Create or update GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.release.outputs.tag }} + generate_release_notes: true + files: firmware/*.bin \ No newline at end of file diff --git a/LegoTrain/Dockerfile b/LegoTrain/Dockerfile index d4baecc..b31469f 100644 --- a/LegoTrain/Dockerfile +++ b/LegoTrain/Dockerfile @@ -6,7 +6,7 @@ EXPOSE 80 EXPOSE 443 EXPOSE 2024/udp -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["LegoTrain/LegoTrain.csproj", "LegoTrain/"] RUN dotnet restore "LegoTrain/LegoTrain.csproj" diff --git a/LegoTrain/Dockerfile.arm32 b/LegoTrain/Dockerfile.arm32 index 5d585ef..a059a8e 100644 --- a/LegoTrain/Dockerfile.arm32 +++ b/LegoTrain/Dockerfile.arm32 @@ -8,7 +8,7 @@ EXPOSE 443 EXPOSE 2024/udp -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["LegoTrain/LegoTrain.csproj", "LegoTrain/"] RUN dotnet restore "LegoTrain/LegoTrain.csproj" diff --git a/LegoTrain/Dockerfile.arm64 b/LegoTrain/Dockerfile.arm64 index 32a6ee6..909667c 100644 --- a/LegoTrain/Dockerfile.arm64 +++ b/LegoTrain/Dockerfile.arm64 @@ -5,7 +5,7 @@ EXPOSE 80 EXPOSE 443 EXPOSE 2024/udp -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["LegoTrain/LegoTrain.csproj", "LegoTrain/"] RUN dotnet restore "LegoTrain/LegoTrain.csproj" diff --git a/README.md b/README.md index a62bb4d..bbff568 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ IR Lego Power Function for train, switch control with servo motor, light signal using remote wifi controlled MCU based on [ESP32-C3 super mini](https://www.bing.com/search?q=esp32-c3+super+mini&qs=n&form=QBRE&sp=-1&lq=0&pq=esp32-c3+super+mini&sc=9-19&sk=&cvid=465952776C5548268A0AC7C84DE5E692&ghsh=0&ghacc=0&ghpl=) running [.NET nanoFramework](https://www.nanoframework.net)! +## Releases + +Push the `publish` tag, or a tag prefixed with `publish-`, to create a GitHub Release. Publishing a GitHub Release manually also starts the publishing workflow. + +Each release contains the `LegoInfrared.bin`, `SignalSwitch.bin`, and `TrainDetect.bin` firmware files. Docker images for `amd64`, `arm32`, and `arm64` are published to `ghcr.io//` with tags in the form `-`. + One LegoInfrared module allows to control any Lego Power Function and support all Lego modes including exclusive modes which can't be done thru any of the Lego remote control through API. Please refer to the [LegoInfrared project](https://github.com/Ellerbach/LegoInfrared) for more details. In our case, this module will control trains. Specific module allow to control servo motors used to pilot Lego switches and to pilot red/green signal lights. @@ -57,11 +63,40 @@ docker run -d \ ## Building and pushing the containers -You can use the `build_docker` files either on Windows with Powershell if you have Docker desktop installed either the Bash one on WSL or any Linux/Mac environement with Docker/Podman installed. +The Dockerfiles use `FROM --platform=$BUILDPLATFORM` so the .NET application is compiled on the host architecture while the final image targets the selected architecture. `BUILDPLATFORM` is set automatically by Docker BuildKit; do not set it manually. + +Docker Desktop includes Buildx. On Linux, install the Docker Buildx plugin, then verify that it is available: + +```bash +docker buildx version +docker buildx inspect --bootstrap +``` + +The helper scripts build one architecture at a time and tag the image as `-`: + +```powershell +./build-docker.ps1 -Platform amd64 -Version 1.0 +./build-docker.ps1 -Platform arm64 -Version 1.0 +./build-docker.ps1 -Platform arm32 -Version 1.0 +``` + +```bash +./build-docker.sh --platform amd64 --version 1.0 +./build-docker.sh --platform arm64 --version 1.0 +./build-docker.sh --platform arm32 --version 1.0 +``` + +To build directly with Buildx, use the matching platform and Dockerfile. `--load` imports the single-platform result into the local Docker image store: + +```bash +docker buildx build --load --platform linux/amd64 -f LegoTrain/Dockerfile -t legotrain:amd64-1.0 . +docker buildx build --load --platform linux/arm64 -f LegoTrain/Dockerfile.arm64 -t legotrain:arm64-1.0 . +docker buildx build --load --platform linux/arm/v7 -f LegoTrain/Dockerfile.arm32 -t legotrain:arm32-1.0 . +``` -You can specify the platform you want to build, the tag for the image and the version and even if you want to push the image to your prefered registry 😊. +Add `--push` to either helper script to publish to its configured registry. For direct Buildx commands, replace `--load` with `--push` and use a registry-qualified image name. -For convenience a version for arm64, arm32 and amd64 is pushed on `doker.io/ellerbach/legotrain`. The image tag follow the format: platfrom-verion.minor. For example `arm32-1.0` +Published images are available from `docker.io/ellerbach/legotrain` for `amd64`, `arm64`, and `arm32`. ## Using the API diff --git a/TrainDetect/Models/Detector.cs b/TrainDetect/Models/Detector.cs index 83b94eb..2ab92af 100644 --- a/TrainDetect/Models/Detector.cs +++ b/TrainDetect/Models/Detector.cs @@ -18,7 +18,7 @@ public class Detector : IDisposable private AdcController _controller = new AdcController(); private AdcChannel _channel; private Thread _thread; - private volatile bool _running; + private bool _running; /// /// Delegate for detector events. From 9bb02d4ce08b02844d4d8a3647b8ddcb4b17b224 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 20 Aug 2026 17:43:53 -0700 Subject: [PATCH 2/3] fixes --- .github/dependabot.yml | 7 ++++++- .github/workflows/ci.yml | 21 ++++++++++++++++++--- .github/workflows/release.yml | 25 ++++++++++++++++++------- README.md | 4 ++-- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5b94d23..1821e94 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,12 @@ version: 2 updates: - package-ecosystem: nuget - directory: / + directories: + - /DiscoveryMessageTests + - /LegoInfrared + - /LegoTrain + - /SignalSwitch + - /TrainDetect schedule: interval: weekly labels: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 541e98f..27ccf8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,12 +48,27 @@ jobs: docker: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + platform: linux/amd64 + dockerfile: LegoTrain/Dockerfile + - architecture: arm32 + platform: linux/arm/v7 + dockerfile: LegoTrain/Dockerfile.arm32 + - architecture: arm64 + platform: linux/arm64 + dockerfile: LegoTrain/Dockerfile.arm64 steps: - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - - name: Build Docker image + - name: Build ${{ matrix.architecture }} Docker image uses: docker/build-push-action@v6 with: context: . - file: LegoTrain/Dockerfile - push: false \ No newline at end of file + file: ${{ matrix.dockerfile }} + platforms: ${{ matrix.platform }} + push: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e4dcba..aacff54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,13 +9,13 @@ on: types: - published -permissions: - contents: write - packages: write +permissions: {} jobs: firmware: runs-on: windows-latest + permissions: + contents: read steps: - uses: actions/checkout@v4 - uses: NuGet/setup-nuget@v2 @@ -45,6 +45,9 @@ jobs: docker: runs-on: ubuntu-latest + permissions: + contents: read + packages: write strategy: fail-fast: false matrix: @@ -71,9 +74,12 @@ jobs: - name: Resolve image metadata id: metadata shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - if [[ "${{ github.event_name }}" == "release" ]]; then - version="${{ github.event.release.tag_name }}" + if [[ "$EVENT_NAME" == "release" ]]; then + version="$RELEASE_TAG" else version="${GITHUB_REF_NAME}" fi @@ -93,6 +99,8 @@ jobs: - firmware - docker runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/download-artifact@v4 with: @@ -101,9 +109,12 @@ jobs: - name: Resolve release tag id: release shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - if [[ "${{ github.event_name }}" == "release" ]]; then - echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT" + if [[ "$EVENT_NAME" == "release" ]]; then + echo "tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" else echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" fi diff --git a/README.md b/README.md index bbff568..4a1f6f2 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ docker run -d \ --restart unless-stopped \ --network host \ -v $(pwd)/LegoTrain/config:/app/config \ - ellerbach/legotrain:arm32-1.0 + ghcr.io/ellerbach/legotrain:arm32-1.0 ``` ## Building and pushing the containers @@ -96,7 +96,7 @@ docker buildx build --load --platform linux/arm/v7 -f LegoTrain/Dockerfile.arm32 Add `--push` to either helper script to publish to its configured registry. For direct Buildx commands, replace `--load` with `--push` and use a registry-qualified image name. -Published images are available from `docker.io/ellerbach/legotrain` for `amd64`, `arm64`, and `arm32`. +Release images are available from `ghcr.io/ellerbach/legotrain` for `amd64`, `arm64`, and `arm32`. ## Using the API From 4e5164d9d71d1e325db92aab9c19c81f3aa2d824 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 20 Aug 2026 17:53:53 -0700 Subject: [PATCH 3/3] doc nit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a1f6f2..8e39a67 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ docker run -d \ --restart unless-stopped \ --network host \ -v $(pwd)/LegoTrain/config:/app/config \ - ghcr.io/ellerbach/legotrain:arm32-1.0 + ghcr.io/ellerbach/legotrain:arm32-publish-1.0 ``` ## Building and pushing the containers