name: Build, test and publish on: workflow_call: inputs: openwrt_version: description: OpenWrt version passed as OPENWRT_VERSION build argument (e.g. "master" or "25.12.6") required: true type: string ci_tag: description: Docker Hub tag used for the intermediate CI image (e.g. "ci-openwrt-stable") required: true type: string app_version_prefix: description: Prefix of the u-OS app version. uc-aom-packager accepts decimal numbers only. required: true type: string publish_tags: description: Space-separated Docker Hub tags that are set after the tests passed required: true type: string publish_versioned_tag: description: Additionally publish the tag "openwrt--" required: false type: boolean default: false env: OPENWRT_VERSION: ${{ inputs.openwrt_version }} CI_IMAGE: albrechtloh/openwrt-docker:${{ inputs.ci_tag }} jobs: build: name: Build runs-on: ubuntu-26.04 services: registry: image: registry:2 ports: - 5000:5000 steps: - name: Checkout uses: actions/checkout@v7 - name: Set Swap Space uses: pierotofy/set-swap-space@master with: swap-size-gb: 10 - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 with: driver-opts: network=host # Important to access local Docker registry - name: Login into Docker Hub uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build Docker image uses: docker/build-push-action@v7 env: DOCKER_BUILD_RECORD_UPLOAD: false with: context: . push: true provenance: false platforms: | linux/x86_64 linux/arm64 build-args: | OPENWRT_VERSION=${{ env.OPENWRT_VERSION }} BUILDKIT_MULTI_PLATFORM=1 tags: | ${{ env.CI_IMAGE }} localhost:5000/local/openwrt-docker:latest - name: Inspect run: | docker buildx imagetools inspect localhost:5000/local/openwrt-docker:latest - name: Build and export uses: docker/build-push-action@v7 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false with: tags: openwrt-docker:latest build-args: OPENWRT_VERSION=${{ env.OPENWRT_VERSION }} allow: security.insecure,network.host outputs: type=docker,dest=/tmp/openwrt-docker-image.tar - name: Upload artifact uses: actions/upload-artifact@v7 with: name: OpenWrt Docker Image path: /tmp/openwrt-docker-image.tar - name: Login to Weidmueller wmucdev uses: docker/login-action@v4 with: registry: wmucdev.azurecr.io username: ${{ secrets.WMUCDEV_USERNAME }} password: ${{ secrets.WMUCDEV_TOKEN }} - name: Create Weidmueller u-OS app id: create_u-os-app env: APP_VERSION_PREFIX: ${{ inputs.app_version_prefix }} run: | # uc-aom-packager accept only version that has to be greater than the previous one. Lets use the datetime to ensure that. DATETIME=$(date "+%Y%m%d%H%M%S") # uc-aom-packager allow only decimal numbers as version number for an unkonwn reason # see https://github.com/weidmueller/uc-addon/blob/main/uc-manifest.schema-doc.md#version) # So lets convert the git hash into a decimal value GIT_HASH=`git rev-parse --short HEAD` GIT_HASH_DECIMAL=$((16#$GIT_HASH)) # Create app version APP_VERSION=$APP_VERSION_PREFIX.$DATETIME-$GIT_HASH_DECIMAL echo "***** Create manifest from template *****" mv u-os-app/manifest.json.template u-os-app/manifest.json sed -i 's/APPVERSION_TO_INSERT/'"$APP_VERSION"'/g' u-os-app/manifest.json cat u-os-app/manifest.json | grep version echo "***** Run uc-aom-packager to create the u-OS app *****" # uc-aom-packager accepts only multiarch images. "--build-arg BUILDKIT_MULTI_PLATFORM=1" has to set to ensure that. # uc-aom-packager needs to Docker registry credentials in a clear text file. Because of security concerns we use a local registry without credentials here. docker run --rm --network=host --pull=always \ --mount src=$(pwd)/u-os-app,target=/tmp/u-os-app-openwrt,type=bind wmucdev.azurecr.io/u-control/uc-aom-packager:0 \ uc-aom-packager push \ -m /tmp/u-os-app-openwrt \ -s /tmp/u-os-app-openwrt/source-credentials.json \ -t /tmp/u-os-app-openwrt/target-credentials.json \ -v echo "***** Create SWU app file*****" docker run --rm --network=host --pull=always \ --mount src=$(pwd)/u-os-app,target=/tmp/u-os-app-openwrt,type=bind wmucdev.azurecr.io/u-control/uc-aom-packager:0 \ uc-aom-packager \ export \ -t /tmp/u-os-app-openwrt/target-credentials.json \ --version $APP_VERSION \ -o /tmp/u-os-app-openwrt/u-os-app-openwrt_$APP_VERSION.swu \ -v find u-os-app/ - name: Archive artifacts (OpenWrt u-OS app) if: always() && steps.create_u-os-app.outcome == 'success' uses: actions/upload-artifact@v7 with: name: OpenWrt u-OS app path: | u-os-app/u-os-app-openwrt_*.swu/linux-arm64/*.swu u-os-app/u-os-app-openwrt_*.swu/linux-amd64/*.swu if-no-files-found: error sbom: name: SBOM generation needs: build runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 - name: Generate alpine SBOM uses: anchore/sbom-action@v0 with: image: ${{ env.CI_IMAGE }} upload-artifact: false format: cyclonedx-json output-file: alpine_sbom.json - name: Prepare OpenWrt rootfs run: | # Use automated generated image (commented "#image:" lines are not touched) sed -i "s|^\(\s*\)image: .*|\1image: $CI_IMAGE|" docker-compose.yml grep -n "image:" docker-compose.yml # Run OpenWrt container for some time to install additional IPKs set +e # Enable that the next command can fail timeout --preserve-status 60 docker compose up set -e # Revert set +e # Get image from volume sudo find /var/lib/docker/volumes/ IMAGE=$(sudo cat /var/lib/docker/volumes/openwrt-docker_data/_data/current_version) echo $IMAGE # Mount volume sudo ./src/mount_openwrt_squashfs_combined.sh "/var/lib/docker/volumes/openwrt-docker_data/_data/${IMAGE}" - name: Generate OpenWrt SBOM uses: anchore/sbom-action@v0 with: path: /mnt upload-artifact: false format: cyclonedx-json output-file: openwrt_sbom.json - name: Generate web-frontend SBOM uses: anchore/sbom-action@v0 with: path: web-frontend upload-artifact: false format: cyclonedx-json output-file: web-frontend_sbom.json - name: Generate web-backend SBOM uses: anchore/sbom-action@v0 with: path: web-backend upload-artifact: false format: cyclonedx-json output-file: web-backend_sbom.json - name: Archive artifacts (SBOMs) uses: actions/upload-artifact@v7 with: name: Software Bill of Material (SBOM) path: ./*.json if-no-files-found: error test: name: Test needs: build runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 - name: Download artifact uses: actions/download-artifact@v8 with: name: OpenWrt Docker Image path: . - name: Install VMs run: | set -x sudo snap install multipass # need multipass >= 1.15 because of networks support # Workaround for "multipass_root_cert.pem': No such file or directory" issue, see https://github.com/canonical/multipass/issues/5015 sleep 180 # Workaround for 'multipass socket access denied' sudo chmod a+w /var/snap/multipass/common/multipass_socket multipass find # Set up some bridges for the VMs sudo ip link add lannetvm type bridge sudo ip link add wannetvm type bridge sudo ip link set dev lannetvm up sudo ip link set dev wannetvm up ip addr # We need to disable the firewall otherwise the tap devices are not working properly sudo ufw disable # Start VMs # Multipass and the OpenWrt container are using the same MAC QUI 52:54:00 which creates error. Let's use differnet MAC addresses here # Multipass >= 1.17 dropped blueprints (formerly "multipass launch docker"), so install Docker via cloud-init printf '#cloud-config\nruncmd:\n - curl -fsSL https://get.docker.com | sh\n - adduser ubuntu docker\n' | \ multipass launch 24.04 --cpus 2 --memory 4G --disk 40G --timeout 1800 --cloud-init - \ --network name=wannetvm,mac=06:00:00:11:22:33 --network name=lannetvm,mac=06:00:00:44:55:66 --name router-vm multipass mount $GITHUB_WORKSPACE router-vm multipass launch --network wannetvm --name wan-vm multipass launch --network lannetvm --name lan-vm - name: Show VM and and environments variables run: | export multipass exec router-vm -- lsb_release -a multipass info - name: Load image run: | multipass exec router-vm -- docker load --input openwrt-docker-image.tar multipass exec router-vm -- docker image ls -a - name: Prepare VMs run: | # Install python multipass exec router-vm -- sudo apt-get -y install python3-pip python-is-python3 python3-venv # Install everthing for pytest multipass exec router-vm -- find . multipass exec router-vm -- bash -c 'python -m venv ~/.venv' multipass exec router-vm -- bash -c 'source ~/.venv/bin/activate && pip install -r test/requirements.txt' echo '******** router-vm IPs ********' multipass exec router-vm -- ip addr # Prepare WAN VM echo '******** wan-vm IPs ********' # Add IP address multipass exec wan-vm -- sudo ip addr add 192.168.22.1/24 dev eth1 multipass exec wan-vm -- ip addr # Install DHCP server multipass exec wan-vm -- sudo sh -c 'apt-get update && apt-get install -y isc-dhcp-server && echo "default-lease-time 600;\nmax-lease-time 7200;\nsubnet 192.168.22.0 netmask 255.255.255.0 {\n range 192.168.22.100 192.168.22.200;\n option routers 192.168.22.1;\n option domain-name-servers 8.8.8.8, 8.8.4.4;\n option domain-name \"example.org\";\n}" > /etc/dhcp/dhcpd.conf && systemctl enable isc-dhcp-server && systemctl restart isc-dhcp-server' multipass exec wan-vm -- cat /etc/dhcp/dhcpd.conf # Enable NAT multipass exec wan-vm -- sudo modprobe iptable_nat multipass exec wan-vm -- sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE multipass exec wan-vm -- sudo sysctl -w net.ipv4.ip_forward=1 # Prepare LAN VM echo '******** lan-vm IPs ********' multipass exec lan-vm -- sudo ip addr add 172.31.1.2/24 dev eth1 multipass exec lan-vm -- sudo ip addr add 172.31.22.2/24 dev eth1 multipass exec lan-vm -- ip addr # Double check if virtual networks are working echo '******** host IPs ********' ip addr sudo ufw status echo '******** router-vm IPs ********' multipass exec router-vm -- sudo ip addr add 192.168.22.99/24 dev eth1 multipass exec router-vm -- sudo ip addr add 172.31.22.99/24 dev eth2 multipass exec router-vm -- ip addr echo '******** Test connection between router-vm, lan-vm and wan-vm ********' multipass exec router-vm -- ping -c 1 192.168.22.1 # to wan-vm multipass exec router-vm -- ping -c 1 172.31.22.2 # to lan-vm - name: Run tests run: | cat test/tests/docker-compose.yml.template # Run pytest multipass exec router-vm -- bash -c 'source ~/.venv/bin/activate && cd test/ && pytest -v -s --html=report.html --self-contained-html --reruns 3' - name: Archive test report if: always() uses: actions/upload-artifact@v7 with: name: Test report path: test/*.html if-no-files-found: error publish: name: Publish needs: test runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 - name: Login into Docker Hub uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Tag image at Docker Hub env: PUBLISH_TAGS: ${{ inputs.publish_tags }} PUBLISH_VERSIONED_TAG: ${{ inputs.publish_versioned_tag }} run: | TAGS=() for TAG in $PUBLISH_TAGS; do TAGS+=(--tag "albrechtloh/openwrt-docker:$TAG") done if [ "$PUBLISH_VERSIONED_TAG" = "true" ]; then GIT_HASH=`git rev-parse --short HEAD` TAGS+=(--tag "albrechtloh/openwrt-docker:openwrt-$OPENWRT_VERSION-$GIT_HASH") fi echo "Publishing $CI_IMAGE as: ${TAGS[*]}" docker buildx imagetools create "${TAGS[@]}" "$CI_IMAGE"