diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ad18704 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.github +.cursor +.direnv +.envrc +.env* +*.out +result +dist +scenarios +examples +docs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c3679db --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,101 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + validate: + name: Validate Flake + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v31 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate flake + run: nix flake check --no-build + + check: + name: ${{ matrix.check }} + runs-on: ubuntu-latest + needs: [validate] + strategy: + fail-fast: false + matrix: + check: [tidy, lint, fmt, lint-md] + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v31 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run ${{ matrix.check }} + run: nix run .#${{ matrix.check }} + + - name: Verify no uncommitted changes + if: matrix.check == 'tidy' || matrix.check == 'fmt' + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "::error::Run 'nix run .#${{ matrix.check }}' and commit changes" + git diff + exit 1 + fi + + unit-test: + name: Unit Tests + runs-on: ubuntu-latest + needs: [check] + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v31 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run unit tests with race detector + run: nix run .#test-unit + + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-unit + path: coverage-unit.out + retention-days: 1 + + report: + name: Coverage Report + runs-on: ubuntu-latest + needs: [unit-test] + if: always() && needs.unit-test.result == 'success' + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Download unit test coverage + uses: actions/download-artifact@v4 + with: + name: coverage-unit + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + use_oidc: true + fail_ci_if_error: true + files: ./coverage-unit.out + flags: unittests + name: deployah-unit + disable_search: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a74067f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,103 @@ +name: Release + +on: + push: + branches: [main] + tags: ["v*"] + +permissions: + contents: write + packages: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: deployah/deployah + # 'latest' only on stable tags (no pre-release suffix like -alpha, -beta, -rc) + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.ref_type == 'tag' && !contains(github.ref_name, '-') }} + type=edge,branch=${{ github.event.repository.default_branch }} + flavor: | + latest=false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + buildkitd-config-inline: | + [worker.oci] + max-parallelism = 10 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build artifacts + if: startsWith(github.ref, 'refs/tags/') + uses: docker/bake-action@v6 + with: + targets: artifact + provenance: false + + - name: Move artifacts + if: startsWith(github.ref, 'refs/tags/') + run: | + find ./dist -type f -not -name "SHA256SUMS" -exec mv {} ./dist/ \; + find ./dist -name "SHA256SUMS" -exec cat {} \; | sort -u > ./dist/SHA256SUMS.combined + mv ./dist/SHA256SUMS.combined ./dist/SHA256SUMS + find ./dist -type d -empty -delete + + - name: Upload artifacts + if: startsWith(github.ref, 'refs/tags/') + uses: actions/upload-artifact@v4 + with: + name: deployah-artifacts + path: ./dist/* + if-no-files-found: error + + - name: Build and push image + uses: docker/bake-action@v6 + with: + targets: image + push: true + sbom: true + provenance: true + files: | + ./docker-bake.hcl + cwd://${{ steps.meta.outputs.bake-file }} + + - name: Sync Docker Hub description + uses: peter-evans/dockerhub-description@v4 + if: github.ref_name == github.event.repository.default_branch + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + short-description: ${{ github.event.repository.description }} + + - name: GitHub Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + draft: true + generate_release_notes: true + files: | + dist/*.tar.gz + dist/*.zip + dist/*.sha256sum + dist/SHA256SUMS + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 3c86c0b..bfd5be1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ go.work.sum # Kind cluster config .kubeconfig +# Docker Buildx artifact output +/dist + # Coverage profiles coverage*.out coverage*.html diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2bfc834 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +# syntax=docker/dockerfile:1 +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1 AS xx + +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS base +ENV GO111MODULE=on +ENV CGO_ENABLED=0 +COPY --from=xx / / +RUN apk add --update --no-cache build-base coreutils git zip +WORKDIR /src + +FROM base AS build +ARG TARGETPLATFORM +RUN --mount=type=bind,target=/src,rw \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + xx-go build -trimpath -ldflags="-s -w" -o /usr/bin/deployah . \ + && xx-verify --static /usr/bin/deployah + +FROM scratch AS binary +COPY --from=build /usr/bin/deployah / + +FROM base AS releaser +ARG TARGETOS +ARG TARGETARCH +ARG TARGETVARIANT +WORKDIR /work +RUN --mount=from=binary,target=/build \ + --mount=type=bind,target=/src \ + mkdir -p /out \ + && cp /build/deployah README.md LICENSE . \ + && if [ "$TARGETOS" = "windows" ]; then \ + zip -q "/out/deployah-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}.zip" \ + deployah README.md LICENSE; \ + else \ + tar -czvf "/out/deployah-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}.tar.gz" \ + deployah README.md LICENSE; \ + fi \ + && cd /out \ + && sha256sum "deployah-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}."* \ + > "deployah-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}.sha256sum" \ + && sha256sum "deployah-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}."* >> "SHA256SUMS" + +FROM scratch AS artifact +COPY --from=releaser /out / + +FROM alpine:3.22 +RUN apk add --update --no-cache ca-certificates tzdata +COPY --from=binary /deployah /usr/bin/deployah +ENTRYPOINT ["deployah"] +CMD ["help"] diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..5ff8ffa --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,25 @@ +// Special target: https://github.com/docker/metadata-action#bake-definition +target "docker-metadata-action" {} + +target "image" { + inherits = ["docker-metadata-action"] + platforms = [ + "linux/amd64", + "linux/arm64", + "linux/arm/v7", + ] +} + +target "artifact" { + target = "artifact" + output = ["./dist"] + platforms = [ + "linux/amd64", + "linux/arm64", + "linux/arm/v7", + "darwin/amd64", + "darwin/arm64", + "windows/amd64", + "windows/arm64", + ] +}