From 422e229a286aa2b833b9a744ce237d00fbf5224d Mon Sep 17 00:00:00 2001 From: Wouter Born Date: Tue, 14 Apr 2026 16:27:11 +0200 Subject: [PATCH] Improve hawkbit Docker CI for PRs, releases, and fork compatibility - add pull_request support for image validation without publishing - add workflow_dispatch support for manual runs on main or tags - make image naming fork-friendly via repository owner fallback - centralize image tag and version computation in a metadata step - skip DockerHub login and publishing for PR builds - support develop tagging on main branch builds - support versioned and latest tags for release builds - fail fast for manual runs on unsupported refs - build the image locally for Anchore/Grype scanning - upload SARIF results only for non-PR runs, and only for workflow_dispatch on main - publish multi-arch amd64/arm64 images for trusted runs - add GitHub Actions cache usage for Docker builds - pin GitHub Actions to commit SHAs This makes the hawkbit image workflow safer for forks and pull requests, keeps release and manual publishing predictable, and adds vulnerability scanning to the CI flow without publishing images from untrusted runs. --- .github/release.yml | 17 ++++ .github/workflows/hawkbit.yml | 142 ++++++++++++++++++++++++---------- .github/workflows/release.yml | 65 ++++++++++++++++ 3 files changed, 183 insertions(+), 41 deletions(-) create mode 100644 .github/release.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..26adb1d --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,17 @@ +changelog: + categories: + - title: 🎉 New features + labels: + - Feature + - title: ⭐ Enhancements + labels: + - Enhancement + - title: 🐞 Bug fixes + labels: + - Bug + - title: 📝 Documentation + labels: + - Documentation + - title: Other changes + labels: + - "*" diff --git a/.github/workflows/hawkbit.yml b/.github/workflows/hawkbit.yml index 633fe45..87c3be9 100644 --- a/.github/workflows/hawkbit.yml +++ b/.github/workflows/hawkbit.yml @@ -1,86 +1,146 @@ name: Docker Image on: + # When a release is published release: types: [published] + + # Push excluding tags and Markdown-only changes push: branches: - main tags-ignore: - - "*.*" + - '*.*' + paths-ignore: + - '**/*.md' + + # Validate pull requests without publishing + pull_request: + branches: + - main paths-ignore: - - "**/*.md" + - '**/*.md' + + # Manual trigger + workflow_dispatch: permissions: contents: read security-events: write concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.ref }} cancel-in-progress: true +env: + IMAGE_NAME: ${{ vars.DOCKERHUB_NAMESPACE || github.repository_owner }}/hawkbit-update-server + PLATFORM: linux/amd64,linux/arm64 + jobs: image_hawkbit: + name: Build images runs-on: ubuntu-latest - env: - PLATFORM: linux/amd64,linux/arm64 - IMAGE_NAME: openremote/hawkbit-update-server steps: - - name: Set tags - id: set-tags - run: | - if [ -z "$RELEASE_TAG" ]; then - echo "DOCKER_TAG_ARGS=-t ${IMAGE_NAME}:develop" >> "$GITHUB_ENV" - echo "dockerImage=${IMAGE_NAME}:develop" >> "$GITHUB_OUTPUT" - else - echo "DOCKER_TAG_ARGS=-t ${IMAGE_NAME}:latest -t ${IMAGE_NAME}:$RELEASE_TAG" >> "$GITHUB_ENV" - echo "dockerImage=${IMAGE_NAME}:$RELEASE_TAG" >> "$GITHUB_OUTPUT" - fi - env: - RELEASE_TAG: ${{ github.event.release.tag_name }} - - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: all - name: Set up Buildx - id: buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 - with: - version: latest - install: true + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - name: Available platforms - run: echo "${{ steps.buildx.outputs.platforms }}" - - - name: Login to DockerHub - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets._TEMP_DOCKERHUB_USER }} password: ${{ secrets._TEMP_DOCKERHUB_PASSWORD }} - - name: Build and push images + - name: Compute image metadata + id: meta + shell: bash run: | - docker build --build-arg GIT_COMMIT=${{ github.sha }} --push --platform $PLATFORM $DOCKER_TAG_ARGS . + VERSION="" + IS_VERSIONED="false" + PRIMARY_TAG="" + PUSH_TAGS="" + + if [[ "${{ github.event_name }}" == "release" ]]; then + VERSION="${{ github.event.release.tag_name }}" + IS_VERSIONED="true" + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ "${{ github.ref_type }}" == "tag" ]]; then + VERSION="${{ github.ref_name }}" + IS_VERSIONED="true" + elif [[ "${{ github.ref }}" != "refs/heads/main" ]]; then + echo "workflow_dispatch must be run on main or on a tag" + exit 1 + fi + fi + + if [[ "$IS_VERSIONED" == "true" ]]; then + PRIMARY_TAG="${IMAGE_NAME}:${VERSION}" + PUSH_TAGS="${IMAGE_NAME}:${VERSION} + ${IMAGE_NAME}:latest" + elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + PRIMARY_TAG="${IMAGE_NAME}:develop" + PUSH_TAGS="${IMAGE_NAME}:develop" + else + PRIMARY_TAG="${IMAGE_NAME}:pr-${{ github.event.pull_request.number || github.run_number }}" + PUSH_TAGS="${PRIMARY_TAG}" + fi - - name: Scan hawkbit docker image - uses: anchore/scan-action@3c9a191a0fbab285ca6b8530b5de5a642cba332f # v7.2.2 + { + echo "version=$VERSION" + echo "is_versioned=$IS_VERSIONED" + echo "primary_tag=$PRIMARY_TAG" + echo "push_tags<> "$GITHUB_OUTPUT" + + - name: Build image locally for scanning + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + platforms: linux/amd64 + load: true + push: false + tags: ${{ steps.meta.outputs.primary_tag }} + build-args: | + GIT_COMMIT=${{ github.sha }} + cache-from: type=gha,scope=hawkbit-amd64 + cache-to: type=gha,mode=max,scope=hawkbit-amd64 + + - name: Scan hawkbit Docker image + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 id: anchore-scan with: - image: ${{ steps.set-tags.outputs.dockerImage }} + image: ${{ steps.meta.outputs.primary_tag }} fail-build: false severity-cutoff: critical - name: Upload Anchore scan SARIF report - if: ${{ !cancelled() }} - uses: github/codeql-action/upload-sarif@c8e3174949dcd2ceb71718aeaa53fee4dc9052f2 # v4.31.7 + if: ${{ !cancelled() && github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main') }} + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: ${{ steps.anchore-scan.outputs.sarif }} + category: grype-hawkbit + + - name: Build and push multi-arch images + if: github.event_name != 'pull_request' + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + platforms: ${{ env.PLATFORM }} + push: true + tags: ${{ steps.meta.outputs.push_tags }} + build-args: | + GIT_COMMIT=${{ github.sha }} + cache-from: type=gha,scope=hawkbit-multiarch + cache-to: type=gha,mode=max,scope=hawkbit-multiarch - - name: Inspect Anchore scan SARIF report - if: ${{ !cancelled() }} - run: cat "${{ steps.anchore-scan.outputs.sarif }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5449bd9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +name: Release + +on: + workflow_dispatch: + inputs: + VERSION: + description: 'Release version/tag to create (example: 1.0.0.0)' + type: string + required: true + +permissions: + actions: write + contents: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Validate version + shell: bash + run: | + if [[ -z "${VERSION}" ]]; then + echo "VERSION is required" + exit 1 + fi + + if git ls-remote --tags origin "refs/tags/${VERSION}" | grep -q "refs/tags/${VERSION}$"; then + echo "Tag ${VERSION} already exists on origin" + exit 1 + fi + env: + VERSION: ${{ github.event.inputs.VERSION }} + + - name: Create and push tag + shell: bash + run: | + git tag "${VERSION}" + git push origin "${VERSION}" + env: + VERSION: ${{ github.event.inputs.VERSION }} + + - name: Create GitHub release + shell: bash + run: | + gh release create "${VERSION}" \ + --generate-notes \ + --title "${VERSION}" + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ github.event.inputs.VERSION }} + + - name: Trigger Docker workflow + shell: bash + run: | + gh workflow run hawkbit.yml --ref "${VERSION}" + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ github.event.inputs.VERSION }}