# CI and releases (docs/milestones/R1.md). # A push to main: the host tests, with their coverage of lib/, and the README's badges # published to the branch `badges`. # A pull request: the same tests and coverage, then the firmware (from the build cache). # A branch's pushes run nothing by themselves: its pull request runs, once. # A tag v*: the tests, then the firmware built once, clean, signed and published as a # Gitea release. # Run by hand: the release of a tag that exists already (the ones from before CI). # # The job runs in a plain Python image, as scripts/ci.sh does on a developer's machine, with the # toolchains in a Docker volume the runner allows (container.valid_volumes: roro9stack-pio): that # volume is the cache. No JavaScript actions, so the image needs no Node: the checkout is git. # # What the volume keeps between runs, and what makes each go stale (issue #74, R1.md): # /pio/packages, /pio/platforms toolchains and the framework: by their versions in platformio.ini # .../framework-arduinoespressif32-libs/.roro-sdkconfig.defaults # the mark that the framework is already rebuilt with our SDK settings: the # project's sdkconfig.defaults, which isn't in git, so that every fresh # checkout rebuilt the framework (260 s). The platform checks its hash # against platformio.ini's settings, and rebuilds if they differ. It is kept # inside the libraries it describes, so it goes when they are reinstalled. # /pio/ci/build-cache PlatformIO's build cache (SCons): objects by the signature of their # sources and command line. For pull requests only: a release compiles # its own sources from nothing. # /pio/ci/ccache the host tests' objects (they're built for coverage, which the build # cache can't keep: it would lose the .gcno files) # /pio/ci/venv PlatformIO and gcovr: delete the folder to upgrade them # To start from nothing (a slow run, about 7 minutes): delete /pio/ci and that .roro-sdkconfig.defaults file. name: CI on: push: branches: [main] # other branches are tested by their pull request: one run, not two tags: ['v*'] # A change that touches nothing but the site and the documents it is built from runs the Site # workflow only (a tag always runs this one: Gitea doesn't apply path filters to tags). paths-ignore: ['site/**', 'docs/**', 'README.md', 'CONTEXT.md'] pull_request: paths-ignore: ['site/**', 'docs/**', 'README.md', 'CONTEXT.md'] workflow_dispatch: inputs: tag: description: An existing tag to build and publish as a release required: true jobs: build: runs-on: ubuntu # A pull request from a fork would run someone else's code on our runner: not without us (Q154). if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository container: image: python:3.12-slim volumes: - roro9stack-pio:/pio env: PLATFORMIO_CORE_DIR: /pio RORO_NO_DOCKER: 1 SDK_MARK: /pio/packages/framework-arduinoespressif32-libs/.roro-sdkconfig.defaults CCACHE_DIR: /pio/ci/ccache CCACHE_MAXSIZE: 1G steps: - name: Tools run: | apt-get update -qq apt-get install -y -qq --no-install-recommends git build-essential openssl ccache >/dev/null mkdir -p /pio/ci if [ ! -x /pio/ci/venv/bin/pio ]; then python -m venv /pio/ci/venv /pio/ci/venv/bin/pip install -q --no-cache-dir platformio gcovr fi ln -sf /pio/ci/venv/bin/pio /pio/ci/venv/bin/gcovr /usr/local/bin/ pio --version; df -h /pio | tail -1; du -sh /pio/ci/* 2>/dev/null || true # The build cache only grows: start it again past 3 GB (a full set of objects is 160 MB, and each run adds about 40). if [ "$(du -sm /pio/ci/build-cache 2>/dev/null | cut -f1)" -gt 3072 ] 2>/dev/null; then rm -rf /pio/ci/build-cache; fi - name: Check out run: | find . -mindepth 1 -maxdepth 1 -exec rm -rf {} + git config --global --add safe.directory '*' git init -q . git remote add origin "${{ github.server_url }}/${{ github.repository }}.git" git fetch -q --tags origin '+refs/heads/*:refs/remotes/origin/*' '+refs/pull/*/head:refs/remotes/pull/*' git checkout -q --detach "${{ github.sha }}" git describe --tags --always - name: Host tests, and their coverage of lib/ if: github.event_name != 'workflow_dispatch' run: | export PATH="/usr/lib/ccache:$PATH" # gcc and g++ through ccache scripts/coverage.sh ccache -s | grep -E 'Hits|Misses' | head -2 # A pull request only: a tag's firmware is built once, by the release step below. - name: The firmware if: github.event_name == 'pull_request' env: PLATFORMIO_BUILD_CACHE_DIR: /pio/ci/build-cache run: | [ ! -f "$SDK_MARK" ] || cp "$SDK_MARK" sdkconfig.defaults scripts/ci.sh builds cp sdkconfig.defaults "$SDK_MARK" # what the framework in the volume is rebuilt with, now # The README's badges are files on a branch of their own, replaced at each push to main and # at each tag (the release badge says which tag is the latest) - name: Publish the badges if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref_type == 'tag') env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | rm -rf /tmp/badges && mkdir /tmp/badges cp .pio/coverage/coverage.svg .pio/coverage/summary.json /tmp/badges/ scripts/coverage_badge.py --plain release "$(git describe --tags --abbrev=0)" /tmp/badges/release.svg cd /tmp/badges git init -q -b badges . git add . git -c user.name="roro9stack CI" -c user.email="ci@git.twis.la" commit -q -m "Coverage of ${{ github.ref_name }} at ${{ github.sha }}" git push -q --force "$(echo "${{ github.server_url }}" | sed "s#://#://ci:${GITEA_TOKEN}@#")/${{ github.repository }}.git" badges - name: Which release id: release run: | if [ "${{ github.event_name }}" = workflow_dispatch ]; then echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" elif [ "${{ github.ref_type }}" = tag ]; then echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" fi - name: Build and sign the release if: steps.release.outputs.tag != '' env: OTA_SIGNING_KEY: ${{ secrets.OTA_SIGNING_KEY }} run: | # The sources of the tag in a clone of their own; the tools are this commit's. rm -rf /tmp/release-src dist git clone -q . /tmp/release-src git -C /tmp/release-src checkout -q --detach "refs/tags/${{ steps.release.outputs.tag }}" # The key exists as a file only while this step runs, in a container that goes with the job. umask 077 export RORO_OTA_KEY="$(mktemp)" trap 'rm -f "$RORO_OTA_KEY"' EXIT printf '%s\n' "$OTA_SIGNING_KEY" > "$RORO_OTA_KEY" umask 022 # The framework rebuilt with our settings is reused if it matches (the platform checks); # the release's own sources are compiled from nothing, with no build cache. [ ! -f "$SDK_MARK" ] || cp "$SDK_MARK" /tmp/release-src/sdkconfig.defaults scripts/release_build.sh /tmp/release-src dist # An old tag has no SDK settings of its own, and no sdkconfig.defaults afterwards: nothing to mark. [ ! -f /tmp/release-src/sdkconfig.defaults ] || [ ! -d "$(dirname "$SDK_MARK")" ] || cp /tmp/release-src/sdkconfig.defaults "$SDK_MARK" - name: Publish the release if: steps.release.outputs.tag != '' env: GITEA_API: ${{ github.server_url }}/api/v1 GITEA_REPO: ${{ github.repository }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: scripts/release_publish.py dist