diff --git a/.github/workflows/dagger-lint.yaml b/.github/workflows/dagger-lint.yaml new file mode 100644 index 00000000..0b1a3e15 --- /dev/null +++ b/.github/workflows/dagger-lint.yaml @@ -0,0 +1,18 @@ +name: 'Code Style Review' +on: + push: + branches: + - dagger-ci +jobs: + lint: + runs-on: 'ubuntu-latest' + steps: + - uses: actions/checkout@v4 + - name: 'lint' + uses: dagger/dagger-for-github@v7.0.1 + with: + verb: call + module: ci + args: lint --pass=true --dir=./ + version: '0.15.3' + cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} diff --git a/.github/workflows/dagger-test.yaml b/.github/workflows/dagger-test.yaml new file mode 100644 index 00000000..e09a07b3 --- /dev/null +++ b/.github/workflows/dagger-test.yaml @@ -0,0 +1,26 @@ +name: 'Test' +on: + push: + branches: + - dagger-ci +jobs: + test: + runs-on: 'ubuntu-latest' + steps: + - uses: actions/checkout@v4 + - name: Test + uses: dagger/dagger-for-github@v7.0.1 + with: + verb: call + module: ci + args: test --dir=./ export --path=./test-reports + version: '0.15.3' + cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} + - name: Report + uses: dorny/test-reporter@v1.9.1 + with: + name: Minitests + path: test-reports/*.xml + reporter: java-junit + fail-on-empty: 'false' + fail-on-error: 'true' diff --git a/.github/workflows/dagger.yaml b/.github/workflows/dagger.yaml new file mode 100644 index 00000000..1087986c --- /dev/null +++ b/.github/workflows/dagger.yaml @@ -0,0 +1,49 @@ +name: 'Full Dagger Pipeline' +on: + push: + branches: + - dagger-ci +jobs: + pipeline: + runs-on: 'ubuntu-latest' + steps: + - uses: actions/checkout@v4 + - name: 'pipeline' + uses: dagger/dagger-for-github@v7.0.1 + env: + REGISTRY_ADDRESS: "ttl.sh/ptime/test:latest" + REGISTRY_USERNAME: "joe" + # TODO + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }} + DT_ADDRESS: "https://deptrack-test.ocp.cloudscale.puzzle.ch/api/v1/bom" + DT_PROJECT_UUID: "93394d20-7c48-4ecf-8caa-7fe63ae1a275" + # TODO + DT_API_KEY: ${{ secrets.DT_API_KEY }} + with: + # Dagger Version + version: 0.15.3 + # Dagger CLI Flags + #dagger-flags: # optional, default is --progress plain + # CLI verb (call, run, download, up, functions, shell, query) + verb: call + # The working directory in which to run the Dagger CLI + workdir: . + # Dagger Cloud Token + cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} + # Dagger module to call. Local or Git + module: . + # Arguments to pass to CLI + args: ci-integration --pass=true --dir=./ --registry-address=${{ env.REGISTRY_ADDRESS }} --registry-username=${{ env.REGISTRY_USERNAME }} --registry-password=env:${{ env.REGISTRY_PASSWORD }} --dt-address=${{ env.DT_ADDRESS }} --dt-project-uuid=${{ env.DT_PROJECT_UUID }} --dt-api-key=env:${{ env.DT_API_KEY }} export --path=./reports/ + - name: Unittest Report + uses: dorny/test-reporter@v1.9.1 + with: + name: unit-tests + path: reports/unit-tests/*.xml + reporter: java-junit + fail-on-empty: 'false' + fail-on-error: 'true' + - name: archive pipeline artifacts + uses: actions/upload-artifact@v4 + with: + name: pipeline-artifacts + path: reports diff --git a/.github/workflows/reusable-lint.yaml b/.github/workflows/reusable-lint.yaml deleted file mode 100644 index 96e86a7c..00000000 --- a/.github/workflows/reusable-lint.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: 'Code Style Review' - -on: - workflow_call: - -jobs: - lint: - runs-on: 'ubuntu-latest' - - steps: - - uses: actions/checkout@v4 - - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - uses: reviewdog/action-rubocop@v2 - with: - rubocop_version: gemfile - rubocop_extensions: rubocop-minitest:gemfile rubocop-performance:gemfile rubocop-rails:gemfile - # reporter: github-pr-review - level: error - - - run: 'gem install haml-lint' - - - uses: reviewdog/action-setup@v1 - - - name: 'Run Reviewdog HAML-Lint' - env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }} - run: | - haml-lint | - reviewdog \ - -efm="%f:%l %m" \ - -name="HAML-Lint" \ - -reporter=github-pr-review \ - -level=error \ - -diff="git diff $DIFF_BRANCH" diff --git a/.gitignore b/.gitignore index 51e3c8d0..139d21bd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ /log/*.log /tmp !/tmp/pids +out/ test/coverage test/reports diff --git a/Gemfile b/Gemfile index 8a5a64b5..0585de59 100644 --- a/Gemfile +++ b/Gemfile @@ -119,4 +119,7 @@ group :test do gem 'selenium-webdriver' gem 'webdrivers' gem 'webmock' + gem 'simplecov' + gem 'simplecov-rcov' + gem 'minitest' end diff --git a/Gemfile.lock b/Gemfile.lock index 18a7940b..ac244302 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -221,6 +221,7 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) + docile (1.4.1) drb (2.2.0) ruby2_keywords dry-configurable (1.1.0) @@ -617,6 +618,14 @@ GEM websocket (~> 1.0) sentry-raven (3.1.2) faraday (>= 1.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.1) + simplecov-rcov (0.3.7) + simplecov (>= 0.4.1) + simplecov_json_formatter (0.1.4) simpleidn (0.2.1) unf (~> 0.1.4) snaky_hash (2.0.1) @@ -736,6 +745,7 @@ DEPENDENCIES m matrix mini_racer + minitest minitest-reporters mocha nested_form_fields @@ -778,6 +788,8 @@ DEPENDENCIES selectize-rails selenium-webdriver sentry-raven + simplecov + simplecov-rcov spring swagger-blocks terser diff --git a/ci/.gitattributes b/ci/.gitattributes new file mode 100644 index 00000000..3a454933 --- /dev/null +++ b/ci/.gitattributes @@ -0,0 +1,4 @@ +/dagger.gen.go linguist-generated +/internal/dagger/** linguist-generated +/internal/querybuilder/** linguist-generated +/internal/telemetry/** linguist-generated diff --git a/ci/.gitignore b/ci/.gitignore new file mode 100644 index 00000000..7ebabcc1 --- /dev/null +++ b/ci/.gitignore @@ -0,0 +1,4 @@ +/dagger.gen.go +/internal/dagger +/internal/querybuilder +/internal/telemetry diff --git a/ci/go.mod b/ci/go.mod new file mode 100644 index 00000000..590e413c --- /dev/null +++ b/ci/go.mod @@ -0,0 +1,50 @@ +module dagger/ci + +go 1.23.2 + +require ( + github.com/99designs/gqlgen v0.17.57 + github.com/Khan/genqlient v0.7.0 + github.com/vektah/gqlparser/v2 v2.5.20 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 + go.opentelemetry.io/otel/log v0.8.0 + go.opentelemetry.io/otel/sdk v1.32.0 + go.opentelemetry.io/otel/sdk/log v0.8.0 + go.opentelemetry.io/otel/trace v1.32.0 + go.opentelemetry.io/proto/otlp v1.3.1 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + golang.org/x/sync v0.10.0 + google.golang.org/grpc v1.68.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect + github.com/sosodev/duration v1.3.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 + go.opentelemetry.io/otel/sdk/metric v1.32.0 + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect + google.golang.org/protobuf v1.35.2 // indirect +) + +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 + +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 + +replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.8.0 + +replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.8.0 diff --git a/ci/go.sum b/ci/go.sum new file mode 100644 index 00000000..df529205 --- /dev/null +++ b/ci/go.sum @@ -0,0 +1,85 @@ +github.com/99designs/gqlgen v0.17.57 h1:Ak4p60BRq6QibxY0lEc0JnQhDurfhxA67sp02lMjmPc= +github.com/99designs/gqlgen v0.17.57/go.mod h1:Jx61hzOSTcR4VJy/HFIgXiQ5rJ0Ypw8DxWLjbYDAUw0= +github.com/Khan/genqlient v0.7.0 h1:GZ1meyRnzcDTK48EjqB8t3bcfYvHArCUUvgOwpz1D4w= +github.com/Khan/genqlient v0.7.0/go.mod h1:HNyy3wZvuYwmW3Y7mkoQLZsa/R5n5yIRajS1kPBvSFM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vektah/gqlparser/v2 v2.5.20 h1:kPaWbhBntxoZPaNdBaIPT1Kh0i1b/onb5kXgEdP5JCo= +github.com/vektah/gqlparser/v2 v2.5.20/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f h1:M65LEviCfuZTfrfzwwEoxVtgvfkFkBUbFnRbxCXuXhU= +google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f/go.mod h1:Yo94eF2nj7igQt+TiJ49KxjIH8ndLYPZMIRSiRcEbg0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ci/main.go b/ci/main.go new file mode 100644 index 00000000..4c336045 --- /dev/null +++ b/ci/main.go @@ -0,0 +1,391 @@ +// A generated module for Ci functions +// +// This module has been generated via dagger init and serves as a reference to +// basic module structure as you get started with Dagger. +// +// Two functions have been pre-created. You can modify, delete, or add to them, +// as needed. They demonstrate usage of arguments and return types using simple +// echo and grep commands. The functions can be called from the dagger CLI or +// from one of the SDKs. +// +// The first line in this comment block is a short description line and the +// rest is a long description with more detail on the module's purpose or usage, +// if appropriate. All modules should have a short description. + +package main + +import ( + "context" + "dagger/ci/internal/dagger" + "fmt" + "sync" +) + +type Ci struct{} + +type Results struct { + // haml-lint output as json + LintOutput *dagger.File + // brakeman output as plain text + SecurityScan *dagger.File + // trivy results as json + VulnerabilityScan *dagger.File + // the SBOM + Sbom *dagger.File + // the built image + Image *dagger.Container + // the test reports + TestReports *dagger.Directory +} + +// Returns a Container built from the Dockerfile in the provided Directory +func (m *Ci) Build(_ context.Context, dir *dagger.Directory) *dagger.Container { + return dag.Container(). + WithDirectory("/src", dir). + WithWorkdir("/src"). + Directory("/src"). + DockerBuild() +} + +// Returns the result of haml-lint run against the sources in the provided Directory +func (m *Ci) Lint( + dir *dagger.Directory, + // ignore linter failures + // +optional + // +default=false + pass bool, +) *dagger.File { + container := dag.Container(). + From("ruby:latest"). + WithMountedDirectory("/mnt", dir). + WithWorkdir("/mnt"). + WithExec([]string{"gem", "install", "haml-lint"}) + if pass { + return container. + WithExec([]string{"sh", "-c", "haml-lint -r json . > lint.json || true"}). + File("lint.json") + } + return container. + WithExec([]string{"sh", "-c", "haml-lint -r json . > lint.json"}). + File("lint.json") +} + +// Returns the Sast report as a file +func (m *Ci) Sast(dir *dagger.Directory) *dagger.File { + return dag.Container(). + From("presidentbeef/brakeman:latest"). + WithMountedDirectory("/app", dir). + WithWorkdir("/app"). + WithExec([]string{"/usr/src/app/bin/brakeman"}). + File("/app/brakeman-output.tabs") +} + +// Creates a PostgreSQL service for local testing based on the official image with the provided version. If no version is provided, 'latest' will be used. +func (m *Ci) Postgres( + _ context.Context, + // +optional + // +default="latest" + version string) *dagger.Service { + + return dag.Container(). + From(fmt.Sprintf("postgres:%s", version)). + WithEnvVariable("POSTGRES_PASSWORD", "postgres"). + WithExposedPort(5432). + AsService() +} + +// Creates a memcached service for local testing based on the official image with the provided version. If no version is provided, 'latest' will be used. +func (m *Ci) Memcached( + _ context.Context, + // +optional + // +default="latest" + version string) *dagger.Service { + + return dag.Container(). + From(fmt.Sprintf("memcached:%s", version)). + WithExposedPort(11211). + AsService() +} + +// Creates an SBOM for the container +func (m *Ci) Sbom(container *dagger.Container) *dagger.File { + trivy_container := dag.Container(). + From("aquasec/trivy"). + WithEnvVariable("TRIVY_JAVA_DB_REPOSITORY", "public.ecr.aws/aquasecurity/trivy-java-db") + + trivy := dag.Trivy(dagger.TrivyOpts{ + Container: trivy_container, + DatabaseRepository: "public.ecr.aws/aquasecurity/trivy-db", + }) + + sbom := trivy.Container(container). + Report("cyclonedx"). + WithName("cyclonedx.json") + + return sbom +} + +// Executes the test suite for the Rails application in the provided Directory +func (m *Ci) Test(ctx context.Context, dir *dagger.Directory) *dagger.Directory { + return m.BaseTestContainer(ctx, dir). + WithServiceBinding("postgresql", m.Postgres(ctx, "11")). + WithServiceBinding("memcached", m.Memcached(ctx, "latest")). + WithExec([]string{"bundle", "exec", "rails", "db:create"}). + WithExec([]string{"bundle", "exec", "rails", "db:migrate"}). + WithExec([]string{"bundle", "exec", "rails", "assets:precompile"}). + WithExec([]string{"sh", "-c", "bundle exec rails test -v test/controllers test/domain test/fabricators test/fixtures test/helpers test/mailers test/models test/presenters test/support test/tarantula"}). + Directory("/mnt/test/reports") +} + +// Builds the container and creates an SBOM for it +func (m *Ci) SbomBuild(ctx context.Context, dir *dagger.Directory) *dagger.File { + container := m.Build(ctx, dir) + + return m.Sbom(container) +} + +// Scans the SBOM for vulnerabilities +func (m *Ci) Vulnscan(sbom *dagger.File) *dagger.File { + trivy_container := dag.Container(). + From("aquasec/trivy"). + WithEnvVariable("TRIVY_JAVA_DB_REPOSITORY", "public.ecr.aws/aquasecurity/trivy-java-db") + + trivy := dag.Trivy(dagger.TrivyOpts{ + Container: trivy_container, + DatabaseRepository: "public.ecr.aws/aquasecurity/trivy-db", + }) + + return trivy.Sbom(sbom).Report("json") +} + +// Publish cyclonedx SBOM to Deptrack +func (m *Ci) PublishToDeptrack( + ctx context.Context, + // SBOM file + sbom *dagger.File, + // deptrack address for publishing the SBOM https://deptrack.example.com/api/v1/bom + address string, + // deptrack API key + apiKey *dagger.Secret, + // deptrack project UUID + projectUUID string, +) (string, error) { + return dag.Container(). + From("curlimages/curl"). + WithFile("sbom.json", sbom). + WithExec([]string{"curl", "-X", "POST", "-H", "'Content-Type: multipart/form-data'", "-H", fmt.Sprintf("'X-API-Key: %s'", apiKey), "-F", fmt.Sprintf("'project=%s'", projectUUID), "-F", "bom=@sbom.json", address}). + Stdout(ctx) +} + +// Sign the published image using cosign +func (m *Ci) Sign( + ctx context.Context, + registryUsername string, + registryPassword *dagger.Secret, + // Container image digest to sign + digest string, +) (string, error) { + return dag.Cosign().SignKeyless(ctx, digest, dagger.CosignSignKeylessOpts{RegistryUsername: registryUsername, RegistryPassword: registryPassword}) +} + +// Attests the SBOM using cosign +func (m *Ci) Attest( + ctx context.Context, + registryUsername string, + registryPassword *dagger.Secret, + // Container image digest to attest + digest string, + // SBOM file + predicate *dagger.File, + // SBOM type + sbomType string, +) (string, error) { + return dag.Cosign().AttestKeyless(ctx, digest, predicate, dagger.CosignAttestKeylessOpts{RegistryUsername: registryUsername, RegistryPassword: registryPassword, SbomType: sbomType}) + +} + +// Returns a Container with the base setup for running the rails test suite +func (m *Ci) BaseTestContainer(_ context.Context, dir *dagger.Directory) *dagger.Container { + return dag.Container(). + From("registry.puzzle.ch/docker.io/ruby:3.2"). + WithExec([]string{"useradd", "-m", "-u", "1001", "ruby"}). + WithMountedDirectory("/mnt", dir, dagger.ContainerWithMountedDirectoryOpts{Owner: "ruby"}). + WithWorkdir("/mnt"). + WithEnvVariable("RAILS_DB_HOST", "postgresql"). // This is the service name of the postgres container called by rails + WithEnvVariable("RAILS_TEST_DB_HOST", "postgresql"). + WithEnvVariable("RAILS_TEST_DB_NAME", "postgres"). + WithEnvVariable("RAILS_TEST_DB_USERNAME", "postgres"). + WithEnvVariable("RAILS_TEST_DB_PASSWORD", "postgres"). + WithEnvVariable("RAILS_ENV", "test"). + WithEnvVariable("CI", "true"). + WithEnvVariable("PGDATESTYLE", "German"). + WithEnvVariable("TZ", "Europe/Zurich"). + WithEnvVariable("DEBIAN_FRONTEND", "noninteractive"). + WithEnvVariable("TEST_REPORTS", "true"). + WithExec([]string{"apt-get", "update"}). + WithExec([]string{"apt-get", "-yqq", "install", "libpq-dev", "libvips-dev", "chromium", "graphviz", "imagemagick"}). + WithUser("ruby"). + //WithExec([]string{"gem", "update", "--system"}). + WithExec([]string{"gem", "install", "bundler", "--version", `~>2`}). + WithExec([]string{"bundle", "install", "--jobs", "4", "--retry", "3"}) +} + +// Build and Publish the Container from the Dockerfile in the provided registry +func (m *Ci) BuildAndPublish(ctx context.Context, dir *dagger.Directory, registryAddress string) (string, error) { + return m.Build(ctx, dir).Publish(ctx, registryAddress) +} + +// Publish the provided Container to the provided registry +func (m *Ci) Publish(ctx context.Context, container *dagger.Container, registryAddress string) (string, error) { + return container.Publish(ctx, registryAddress) +} + +// Executes all the steps and returns a Results object +func (m *Ci) Ci( + ctx context.Context, + // source directory + dir *dagger.Directory, + // registry username for publishing the contaner image + registryUsername string, + // registry password for publishing the container image + registryPassword *dagger.Secret, + // registry address registry/repository/image:tag + registryAddress string, + // deptrack address for publishing the SBOM https://deptrack.example.com/api/v1/bom + dtAddress string, + // deptrack project UUID + dtProjectUUID string, + // deptrack API key + dtApiKey *dagger.Secret, + // ignore linter failures + // +optional + // +default=false + pass bool, +) *Results { + lintOutput := m.Lint(dir, pass) + securityScan := m.Sast(dir) + image := m.Build(ctx, dir) + sbom := m.Sbom(image) + vulnerabilityScan := m.Vulnscan(sbom) + testReports := m.Test(ctx, dir) + digest, err := m.Publish(ctx, image, registryAddress) + + if err == nil { + m.PublishToDeptrack(ctx, sbom, dtAddress, dtApiKey, dtProjectUUID) + m.Sign(ctx, registryUsername, registryPassword, digest) + m.Attest(ctx, registryUsername, registryPassword, digest, sbom, "cyclonedx") + } + + return &Results{ + TestReports: testReports, + LintOutput: lintOutput, + SecurityScan: securityScan, + VulnerabilityScan: vulnerabilityScan, + Sbom: sbom, + Image: image, + } +} + +// Executes all the steps and returns a directory with the results +func (m *Ci) CiIntegration( + ctx context.Context, + // source directory + dir *dagger.Directory, + // registry username for publishing the contaner image + registryUsername string, + // registry password for publishing the container image + registryPassword *dagger.Secret, + // registry address registry/repository/image:tag + registryAddress string, + // deptrack address for publishing the SBOM https://deptrack.example.com/api/v1/bom + dtAddress string, + // deptrack project UUID + dtProjectUUID string, + // deptrack API key + dtApiKey *dagger.Secret, + // ignore linter failures + // +optional + // +default=false + pass bool, +) (*dagger.Directory, error) { + var wg sync.WaitGroup + wg.Add(5) + + var lintOutput = func() *dagger.File { + defer wg.Done() + return m.Lint(dir, pass) + }() + + var securityScan = func() *dagger.File { + defer wg.Done() + return m.Sast(dir) + }() + + var vulnerabilityScan = func() *dagger.File { + defer wg.Done() + return m.Vulnscan(m.Sbom(m.Build(ctx, dir))) + }() + + var image = func() *dagger.Container { + defer wg.Done() + return m.Build(ctx, dir) + }() + + var testReports = func() *dagger.Directory { + defer wg.Done() + return m.Test(ctx, dir) + }() + + // This Blocks the execution until its counter become 0 + wg.Wait() + + // Get the names of the files to fail on errors of the functions + lintOutputName, err := lintOutput.Name(ctx) + if err != nil { + return nil, err + } + securityScanName, err := securityScan.Name(ctx) + if err != nil { + return nil, err + } + vulnerabilityScanName, err := vulnerabilityScan.Name(ctx) + if err != nil { + return nil, err + } + + // After linting, scanning and testing is done, we can create the sbom and publish the image + wg.Add(2) + + var sbom = func() *dagger.File { + defer wg.Done() + return m.Sbom(image) + }() + + digest, err := func() (string, error) { + defer wg.Done() + return m.Publish(ctx, image, registryAddress) + }() + + // This Blocks the execution until its counter become 0 + wg.Wait() + + // After publishing the image, we can sign and attest + if err != nil { + return nil, err + } + + m.PublishToDeptrack(ctx, sbom, dtAddress, dtApiKey, dtProjectUUID) + m.Sign(ctx, registryUsername, registryPassword, digest) + m.Attest(ctx, registryUsername, registryPassword, digest, sbom, "cyclonedx") + + sbomName, _ := sbom.Name(ctx) + result_container := dag.Container(). + WithWorkdir("/tmp/out"). + WithFile(fmt.Sprintf("/tmp/out/lint/%s", lintOutputName), lintOutput). + WithFile(fmt.Sprintf("/tmp/out/scan/%s", securityScanName), securityScan). + WithDirectory("/tmp/out/unit-tests/", testReports). + WithFile(fmt.Sprintf("/tmp/out/vuln/%s", vulnerabilityScanName), vulnerabilityScan). + WithFile(fmt.Sprintf("/tmp/out/sbom/%s", sbomName), sbom) + return result_container. + Directory("."), err +} diff --git a/dagger.json b/dagger.json new file mode 100644 index 00000000..a0d7840c --- /dev/null +++ b/dagger.json @@ -0,0 +1,18 @@ +{ + "name": "ci", + "engineVersion": "v0.15.3", + "sdk": "go", + "dependencies": [ + { + "name": "cosign", + "source": "github.com/puzzle/dagger-module-cosign/cosign@v0.1.1", + "pin": "ed23741b9d2aa1fd68e705261814d607ed935b15" + }, + { + "name": "trivy", + "source": "github.com/sagikazarmark/daggerverse/trivy@trivy/v0.5.0", + "pin": "5b826062b6bc1bfbd619aa5d0fba117190c85aba" + } + ], + "source": "ci" +} diff --git a/test/test_helper.rb b/test/test_helper.rb index 77e1e5c4..27211862 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -7,19 +7,19 @@ ENV['RAILS_ENV'] = 'test' -# if ENV['TEST_REPORTS'] -# require 'simplecov' -# require 'simplecov-rcov' -# SimpleCov.coverage_dir 'test/coverage' -# # use this formatter for jenkins compatibility -# SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter -# SimpleCov.command_name 'Unit Tests' -# SimpleCov.start 'rails' - -# require 'minitest/reporters' -# MiniTest::Reporters.use! [MiniTest::Reporters::DefaultReporter.new, -# MiniTest::Reporters::JUnitReporter.new] -# end +if ENV['TEST_REPORTS'] + require 'simplecov' + require 'simplecov-rcov' + SimpleCov.coverage_dir 'test/coverage' + # use this formatter for jenkins compatibility + SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter + SimpleCov.command_name 'Unit Tests' + SimpleCov.start 'rails' + + require 'minitest/reporters' + Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new, + Minitest::Reporters::JUnitReporter.new] +end require File.expand_path('../config/environment', __dir__) Rails.env = 'test'