From ea49dfc640cafeaf1852d99469ba5df412087d5f Mon Sep 17 00:00:00 2001 From: Dmytro Sirant Date: Fri, 3 Apr 2026 13:47:26 +1000 Subject: [PATCH 1/3] ci: Improve GitHub Actions workflows - Add concurrency groups to cancel redundant builds on new commits - Add PR build validation (lint + build without push) - Add hadolint Dockerfile linting - Remove duplicate GHCR build, copy manifest from DockerHub instead - Remove unnecessary QEMU setup (native ARM runners used) - Add GHA cache scoping to prevent matrix cache collisions - Fix release workflow_dispatch condition bug - Add missing PR title types (refactor, test, perf, build, revert) - Add authenticated GitHub API calls in version-check - Add AWS CLI and Helm S3 version checks --- .github/workflows/image-build-push.yaml | 178 ++++++++++-------------- .github/workflows/pr-title.yaml | 25 +--- .github/workflows/release.yaml | 10 +- .github/workflows/version-check.yaml | 22 ++- 4 files changed, 109 insertions(+), 126 deletions(-) diff --git a/.github/workflows/image-build-push.yaml b/.github/workflows/image-build-push.yaml index db07273..f0aa754 100644 --- a/.github/workflows/image-build-push.yaml +++ b/.github/workflows/image-build-push.yaml @@ -3,11 +3,18 @@ name: Docker Build on: push: branches: - - main # Only build and push after PR is merged to main + - main + pull_request: + branches: + - main workflow_dispatch: release: types: [published, edited] +concurrency: + group: docker-build-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read packages: write @@ -17,6 +24,16 @@ env: GHCR_IMAGE: ghcr.io/perun-engineering/aws-helm-kubectl jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Lint Dockerfile + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: Dockerfile + define: runs-on: ubuntu-latest outputs: @@ -27,17 +44,45 @@ jobs: - name: Read environment file id: read_env run: | - # Extract only the KUBERNETES_VERSIONS line and get the array part VERSIONS=$(grep '^KUBERNETES_VERSIONS=' .env | cut -d'=' -f2-) echo "versions=$VERSIONS" >> $GITHUB_OUTPUT + validate: + if: github.event_name == 'pull_request' + needs: [lint, define] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Read environment file + run: cat .env >> ${GITHUB_ENV} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build (validation only) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + push: false + build-args: | + KUBE_VERSION=${{ fromJson(needs.define.outputs.kubernetes_versions)[0] }} + HELM_VERSION=${{ env.HELM_VERSION }} + SOPS_VERSION=${{ env.SOPS_VERSION }} + HELM_SECRETS_VERSION=${{ env.HELM_SECRETS_VERSION }} + HELM_S3_VERSION=${{ env.HELM_S3_VERSION }} + HELMFILE_VERSION=${{ env.HELMFILE_VERSION }} + AWS_CLI_VERSION=${{ env.AWS_CLI_VERSION }} + HELM_DIFF_VERSION=${{ env.HELM_DIFF_VERSION }} + ALPINE_PYTHON=${{ env.ALPINE_PYTHON }} + ALPINE_VERSION=${{ env.ALPINE_VERSION }} + build: - needs: - - define + if: github.event_name != 'pull_request' + needs: [lint, define] strategy: fail-fast: true matrix: - version: ${{fromJson(needs.define.outputs.kubernetes_versions)}} + version: ${{ fromJson(needs.define.outputs.kubernetes_versions) }} platform: - linux/amd64 - linux/arm64 @@ -52,8 +97,6 @@ jobs: uses: actions/checkout@v6 - name: Read environment file run: cat .env >> ${GITHUB_ENV} - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub @@ -61,12 +104,6 @@ jobs: with: username: ${{ vars.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Login to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v6 @@ -76,10 +113,9 @@ jobs: ${{ env.GHCR_IMAGE }} tags: | type=ref,event=branch - type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - - name: Build and push to Docker Hub + - name: Build and push id: build uses: docker/build-push-action@v6 with: @@ -98,43 +134,16 @@ jobs: ALPINE_PYTHON=${{ env.ALPINE_PYTHON }} ALPINE_VERSION=${{ env.ALPINE_VERSION }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=${{ matrix.version }}-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=${{ matrix.version }}-${{ matrix.platform }} - - name: Re-tag and push to GHCR - id: build-ghcr - uses: docker/build-push-action@v6 - with: - context: . - platforms: ${{ matrix.platform }} - outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true - build-args: | - KUBE_VERSION=${{ matrix.version }} - HELM_VERSION=${{ env.HELM_VERSION }} - SOPS_VERSION=${{ env.SOPS_VERSION }} - HELM_SECRETS_VERSION=${{ env.HELM_SECRETS_VERSION }} - HELM_S3_VERSION=${{ env.HELM_S3_VERSION }} - HELMFILE_VERSION=${{ env.HELMFILE_VERSION }} - AWS_CLI_VERSION=${{ env.AWS_CLI_VERSION }} - HELM_DIFF_VERSION=${{ env.HELM_DIFF_VERSION }} - ALPINE_PYTHON=${{ env.ALPINE_PYTHON }} - ALPINE_VERSION=${{ env.ALPINE_VERSION }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - - - name: Export digest (Docker Hub) + - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" echo "$digest" | sed 's/^sha256://' > "/tmp/digests/${digest#sha256:}" - - name: Export digest (GHCR) - run: | - mkdir -p /tmp/digests-ghcr - digest="${{ steps.build-ghcr.outputs.digest }}" - echo "$digest" | sed 's/^sha256://' > "/tmp/digests-ghcr/${digest#sha256:}" - - - name: Upload digest (Docker Hub) + - name: Upload digest uses: actions/upload-artifact@v7 with: name: digests-${{ matrix.version }}-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} @@ -142,22 +151,12 @@ jobs: if-no-files-found: error retention-days: 1 - - name: Upload digest (GHCR) - uses: actions/upload-artifact@v7 - with: - name: digests-ghcr-${{ matrix.version }}-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} - path: /tmp/digests-ghcr/* - if-no-files-found: error - retention-days: 1 - merge: - needs: - - define - - build + needs: [define, build] runs-on: ubuntu-latest strategy: matrix: - version: ${{fromJson(needs.define.outputs.kubernetes_versions)}} + version: ${{ fromJson(needs.define.outputs.kubernetes_versions) }} steps: - name: Download digests uses: actions/download-artifact@v8 @@ -175,21 +174,17 @@ jobs: username: ${{ vars.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Create manifest list and push (Docker Hub) + - name: Create manifest list and push run: | - # First verify the directory and files exist ls -la /tmp/digests - - # Create the manifest list cd /tmp/digests docker buildx imagetools create -t ${{ env.DOCKERHUB_IMAGE }}:${{ matrix.version }} \ $(for digest in *; do echo -n "${{ env.DOCKERHUB_IMAGE }}@sha256:$digest "; done) - - name: Inspect image (Docker Hub) - run: | - docker buildx imagetools inspect ${{ env.DOCKERHUB_IMAGE }}:${{ matrix.version }} + - name: Inspect image + run: docker buildx imagetools inspect ${{ env.DOCKERHUB_IMAGE }}:${{ matrix.version }} - - name: Test final image (Docker Hub) + - name: Test final image run: | echo "Testing final image functionality..." docker run --rm ${{ env.DOCKERHUB_IMAGE }}:${{ matrix.version }} /bin/bash -c " @@ -202,25 +197,22 @@ jobs: echo 'All tools working correctly!' " - merge-ghcr: - needs: - - define - - build + copy-to-ghcr: + needs: [define, merge] runs-on: ubuntu-latest strategy: matrix: - version: ${{fromJson(needs.define.outputs.kubernetes_versions)}} + version: ${{ fromJson(needs.define.outputs.kubernetes_versions) }} steps: - - name: Download digests (GHCR) - uses: actions/download-artifact@v8 - with: - pattern: digests-ghcr-${{ matrix.version }}-* - path: /tmp/digests-ghcr - merge-multiple: true - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ vars.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Login to GitHub Container Registry uses: docker/login-action@v4 with: @@ -228,29 +220,11 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create manifest list and push (GHCR) - run: | - # First verify the directory and files exist - ls -la /tmp/digests-ghcr - - # Create the manifest list - cd /tmp/digests-ghcr - docker buildx imagetools create -t ${{ env.GHCR_IMAGE }}:${{ matrix.version }} \ - $(for digest in *; do echo -n "${{ env.GHCR_IMAGE }}@sha256:$digest "; done) - - - name: Inspect image (GHCR) + - name: Copy manifest to GHCR run: | - docker buildx imagetools inspect ${{ env.GHCR_IMAGE }}:${{ matrix.version }} + docker buildx imagetools create \ + -t ${{ env.GHCR_IMAGE }}:${{ matrix.version }} \ + ${{ env.DOCKERHUB_IMAGE }}:${{ matrix.version }} - - name: Test final image (GHCR) - run: | - echo "Testing final image functionality..." - docker run --rm ${{ env.GHCR_IMAGE }}:${{ matrix.version }} /bin/bash -c " - echo 'Testing tool versions:' && - kubectl version --client && - helm version && - aws --version && - sops --version && - helmfile --version && - echo 'All tools working correctly!' - " + - name: Inspect image + run: docker buildx imagetools inspect ${{ env.GHCR_IMAGE }}:${{ matrix.version }} diff --git a/.github/workflows/pr-title.yaml b/.github/workflows/pr-title.yaml index b7f48b9..fd47934 100644 --- a/.github/workflows/pr-title.yaml +++ b/.github/workflows/pr-title.yaml @@ -16,41 +16,26 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - # Please look up the latest version from - # https://github.com/amannn/action-semantic-pull-request/releases - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - # Configure which types are allowed. - # Default: https://github.com/commitizen/conventional-commit-types types: | fix feat docs ci chore - # Configure that a scope must always be provided. + refactor + test + perf + build + revert requireScope: false - # Configure additional validation for the subject based on a regex. - # This example ensures the subject starts with an uppercase character. subjectPattern: ^[A-Z].+$ - # If `subjectPattern` is configured, you can use this property to override - # the default error message that is shown when the pattern doesn't match. - # The variables `subject` and `title` can be used within the message. subjectPatternError: | The subject "{subject}" found in the pull request title "{title}" didn't match the configured pattern. Please ensure that the subject starts with an uppercase character. - # For work-in-progress PRs you can typically use draft pull requests - # from Github. However, private repositories on the free plan don't have - # this option and therefore this action allows you to opt-in to using the - # special "[WIP]" prefix to indicate this state. This will avoid the - # validation of the PR title and the pull request checks remain pending. - # Note that a second check will be reported if this is enabled. wip: true - # When using "Squash and merge" on a PR with only one commit, GitHub - # will suggest using that commit message instead of the PR title for the - # merge commit, and it's easy to commit this by mistake. Enable this option - # to also validate the commit message for one commit PRs. validateSingleCommit: false diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f0e0cc5..f1dc53b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,6 +9,10 @@ on: branches: - main +concurrency: + group: release + cancel-in-progress: false + permissions: contents: read @@ -20,8 +24,9 @@ jobs: pull-requests: write name: Release runs-on: ubuntu-latest - # Skip running release workflow on forks and only run if Docker Build succeeded - if: github.repository_owner == 'Perun-Engineering' && github.event.workflow_run.conclusion == 'success' + if: >- + github.repository_owner == 'Perun-Engineering' && + (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') steps: - name: Checkout uses: actions/checkout@v6 @@ -46,6 +51,7 @@ jobs: dockerHubDescription: runs-on: ubuntu-latest + if: github.repository_owner == 'Perun-Engineering' steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/version-check.yaml b/.github/workflows/version-check.yaml index 00e21d5..4c5b32d 100644 --- a/.github/workflows/version-check.yaml +++ b/.github/workflows/version-check.yaml @@ -19,12 +19,15 @@ jobs: - name: Check for new versions id: version-check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Checking for new versions..." # Function to get latest GitHub release get_latest_release() { - curl -s "https://api.github.com/repos/$1/releases/latest" | jq -r '.tag_name' | sed 's/^v//' + curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$1/releases/latest" | jq -r '.tag_name' | sed 's/^v//' } # Function to get latest Alpine version @@ -35,7 +38,8 @@ jobs: # Function to get latest Kubernetes version get_latest_kubernetes() { - curl -s "https://api.github.com/repos/kubernetes/kubernetes/releases/latest" | jq -r '.tag_name' | sed 's/^v//' + curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/kubernetes/kubernetes/releases/latest" | jq -r '.tag_name' | sed 's/^v//' } # Read current versions @@ -49,6 +53,8 @@ jobs: LATEST_HELM_SECRETS=$(get_latest_release "jkroepke/helm-secrets") LATEST_HELMFILE=$(get_latest_release "helmfile/helmfile") LATEST_HELM_DIFF=$(get_latest_release "databus23/helm-diff") + LATEST_AWS_CLI=$(get_latest_release "aws/aws-cli") + LATEST_HELM_S3=$(get_latest_release "hypnoglow/helm-s3") echo "Current versions:" echo "Alpine: $ALPINE_VERSION" @@ -57,6 +63,8 @@ jobs: echo "Helm Secrets: $HELM_SECRETS_VERSION" echo "Helmfile: $HELMFILE_VERSION" echo "Helm Diff: $HELM_DIFF_VERSION" + echo "AWS CLI: $AWS_CLI_VERSION" + echo "Helm S3: $HELM_S3_VERSION" echo "Latest versions:" echo "Alpine: $LATEST_ALPINE" @@ -66,6 +74,8 @@ jobs: echo "Helm Secrets: $LATEST_HELM_SECRETS" echo "Helmfile: $LATEST_HELMFILE" echo "Helm Diff: $LATEST_HELM_DIFF" + echo "AWS CLI: $LATEST_AWS_CLI" + echo "Helm S3: $LATEST_HELM_S3" # Check if updates are needed UPDATES_NEEDED="" @@ -94,6 +104,14 @@ jobs: UPDATES_NEEDED="$UPDATES_NEEDED\n- Helm Diff: $HELM_DIFF_VERSION → $LATEST_HELM_DIFF" fi + if [ "$AWS_CLI_VERSION" != "$LATEST_AWS_CLI" ]; then + UPDATES_NEEDED="$UPDATES_NEEDED\n- AWS CLI: $AWS_CLI_VERSION → $LATEST_AWS_CLI" + fi + + if [ "$HELM_S3_VERSION" != "$LATEST_HELM_S3" ]; then + UPDATES_NEEDED="$UPDATES_NEEDED\n- Helm S3: $HELM_S3_VERSION → $LATEST_HELM_S3" + fi + # Check if new Kubernetes version should be added CURRENT_KUBE_VERSIONS=$(echo $KUBERNETES_VERSIONS | jq -r '.[]' | sort -V) LATEST_KUBE_MAJOR_MINOR=$(echo $LATEST_KUBERNETES | cut -d. -f1-2) From 65dc88d734ded4babd92e43cad01838611019144 Mon Sep 17 00:00:00 2001 From: Dmytro Sirant Date: Fri, 3 Apr 2026 13:52:24 +1000 Subject: [PATCH 2/3] ci: Add hadolint config to ignore inapplicable rules --- .hadolint.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .hadolint.yaml diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..95ea4a6 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,7 @@ +ignored: + - DL3018 # Pin versions in apk add - impractical for Alpine, versions change with each release + - DL3013 # Pin versions in pip - builder stage only, pip upgrade is intentional + - DL3042 # Avoid pip cache dir - builder stage only, discarded after build + - DL3059 # Multiple consecutive RUN - intentional separation in builder stage + - SC1091 # Not following sourced file - false positive for venv activate + - DL4006 # Set SHELL -o pipefail - busybox ash doesn't support pipefail From 2438cb62969a06e62fe9a6619262abb32e10d35d Mon Sep 17 00:00:00 2001 From: Dmytro Sirant Date: Fri, 3 Apr 2026 13:57:51 +1000 Subject: [PATCH 3/3] ci: Remove uppercase requirement from PR title validation --- .github/workflows/pr-title.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/pr-title.yaml b/.github/workflows/pr-title.yaml index fd47934..ea8f3bc 100644 --- a/.github/workflows/pr-title.yaml +++ b/.github/workflows/pr-title.yaml @@ -32,10 +32,6 @@ jobs: build revert requireScope: false - subjectPattern: ^[A-Z].+$ - subjectPatternError: | - The subject "{subject}" found in the pull request title "{title}" - didn't match the configured pattern. Please ensure that the subject - starts with an uppercase character. + subjectPattern: ^.+$ wip: true validateSingleCommit: false