From c8e8e10c69975ffabd4c727e3acc045bf18c28ce Mon Sep 17 00:00:00 2001 From: Tronje Krop Date: Wed, 21 Jun 2023 17:36:56 +0200 Subject: [PATCH] feat: initial commit (#0) --- .codacy.yaml | 8 + .github/workflows/go.yaml | 29 + .gitignore | 6 + .gitleaks.toml | 12 + .golangci.yaml | 189 ++++++ .markdownlint.yaml | 35 + LICENSE | 21 + MANUAL.md | 404 ++++++++++++ Makefile | 30 + Makefile.base | 1287 +++++++++++++++++++++++++++++++++++++ Makefile.ext | 8 + Makefile.vars | 30 + README.md | 226 +++++++ go.mod | 7 + go.sum | 2 + main.go | 303 +++++++++ revive.toml | 95 +++ 17 files changed, 2692 insertions(+) create mode 100644 .codacy.yaml create mode 100644 .github/workflows/go.yaml create mode 100644 .gitignore create mode 100644 .gitleaks.toml create mode 100644 .golangci.yaml create mode 100644 .markdownlint.yaml create mode 100644 LICENSE create mode 100644 MANUAL.md create mode 100644 Makefile create mode 100644 Makefile.base create mode 100644 Makefile.ext create mode 100644 Makefile.vars create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 revive.toml diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 0000000..38a96a7 --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,8 @@ +engines: + duplication: + minTokenMatch: 50 + exclude_paths: + - "**/*_test.go" + revive: + exclude_paths: + - "**/mock_*_test.go" diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml new file mode 100644 index 0000000..4b95aa9 --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,29 @@ +name: Go +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ^1.21 + + - name: Build and tests + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + run: make --file=Makefile.base --trace all + + - name: Send coverage report + uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: ./build/test-all.cover + + - name: Release and publish + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + run: |- + make --file=Makefile.base --trace release && \ + make --file=Makefile.base --trace publish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b03c62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.project + +build +run + +mock_*_test.go diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..db40efc --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,12 @@ +[extend] + useDefault = true + +[allowlist] + description = "Whitelist false positives" + regexTarget = "match" + regexes = [ + # mark CDP build secret_version as false possitive since its not a secret + '''(?i)(?:secret_version:)(?:['|\"|\s]{0,5})([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s]{0,5}|$)''', + # mark api-id or api_id or x-api-id as false possitive since they are uuids that are used in OAS and stacksets to track apis + '''(?i)(?:api[_-]id(?:['|\"|\s|:]{0,5}))(?:['|\"|\s]{0,5})([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s]{0,5}|$)''' + ] diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..aefaf4c --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,189 @@ +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + +linters: + # Placeholder for dynamically enabled linters. + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 (we recommend 10-20) + max-same-issues: 10 + + # Use default exclusions for common false positives as defined in + # https://golangci-lint.run/usage/false-positives/#default-exclusions + # Default: true (we use false to sync behavior with Codacy) + exclude-use-default: false + + # Defining manually exclusions that make sense. + exclude-rules: + # Exclude go:generate directives from line length checking. + - source: "^//\\s*go:generate\\s" + linters: [ lll, revive ] + # Exclude magic number in time setups and bit shifting. + - source: "[0-9]+ ?\\* ?time\\.|(<<|>>) ?[0-9]+|[0-9]+ ?(<<|>>)" + linters: [ gomnd ] + # Exclude certain standards from being applied in test. + - path: "_test\\.go" + linters: [ bodyclose, contextcheck, dupl, funlen, goconst, gosec, noctx, + goerr113, wrapcheck ] + + # Exclude error return value check because of too many false positives. + - text: 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked' + linters: [ errcheck ] + # Exclude certain revive standards from being applied in tests. + - path: "_test\\.go" + text: "^(max-public-structs|function-length|cognitive-complexity):" + linters: [ revive ] + # Exclude dots in unfinished thoughts. + - source: "(noinspection|TODO)" + linters: [ godot ] + + +# This contains only configs which differ from defaults. For other configs see +# https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 (we allow up to 20) + max-complexity: 20 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 (we recommend 10.0 as baseline) + package-average: 10.0 + + gocognit: + # Minimal code complexity to report. + # Default: 30 (we recommend 10-20) + min-complexity: 20 + + lll: + # Max line length, lines longer will be reported. '\t' is counted as 1 + # character by default, and can be changed with the tab-width option. + # Default: 120 (we recommend 80 but compromise at 100) + line-length: 100 + # Tab width in spaces. + # Default: 1 (go uses 4 for visualization) + tab-width: 4 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] (but some lll does not need explanation) + allow-no-explanation: [ lll, wrapcheck ] + # Enable to require an explanation of nonzero length after each nolint + # directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being + # suppressed. + # Default: false + require-specific: true + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + - shadow # too strict to always work around + + gosec: + # To specify a set of rules to explicitly exclude. + # Available rules: https://github.com/securego/gosec#available-rules + # Default: [] (issues are fixed) + excludes: [ G105, G307 ] + + gocritic: + # Settings passed to gocritic. The settings key is the name of a supported + # gocritic checker. The list of supported checkers can be find in + # https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + tenv: + # The option `all` will run the linter on the whole test files regardless + # of method signatures. Otherwise, only methods that take `*testing.T`, + # `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + revive: + # Enable all available rules. + # Default: false + enable-all-rules: true + # When set to false, ignores files with "GENERATED" header, similar to golint. + # See https://github.com/mgechev/revive#available-rules for details. + # Default: false + ignore-generated-header: true + # Sets the default severity. + # See https://github.com/mgechev/revive#configuration + # Default: warning + severity: error + + rules: + # No need to enforce a file header. + - name: file-header + disabled: true + # Reports on each file in a package. + - name: package-comments + disabled: true + # Reports on comments not matching the name as first word. + - name: exported + disabled: true + # No need to exclude import shadowing. + - name: import-shadowing + disabled: true + # Fails to exclude nolint directives from reporting. + - name: comment-spacings + disabled: true + # Fails to disable writers that actually cannot return errors. + - name: unhandled-error + disabled: true + # Fails to restrict sufficiently in switches with numeric values. + - name: add-constant + disabled: true + # Rule prevents intentional usage of similar variable names. + - name: flag-parameter + disabled: true + # Rule prevents intentional usage of similar private method names. + - name: confusing-naming + disabled: true + + # Enables a more experienced cyclomatic complexity (we enabled a lot of + # rules to counter-act the complexity trap). + - name: cyclomatic + arguments: [20] + # Enables a more experienced cognitive complexity (we enabled a lot of + # rules to counter-act the complexity trap). + - name: cognitive-complexity + arguments: [20] + # Limit line-length to increase readability. + - name: line-length-limit + arguments: [100] + # We are a bit more relaxed with function length consistent with funlen. + - name: function-length + arguments: [40, 60] + # Limit arguments of functions to the maximum understandable value. + - name: argument-limit + arguments: [6] + # Limit results of functions to the maximum understandable value. + - name: function-result-limit + arguments: [4] + # Raise the limit a bit to allow more complex package models. + - name: max-public-structs + arguments: [8] + # I do not know what I'm doing here... + - name: banned-characters + arguments: ["Ω", "Σ", "σ"] diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..5769f66 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,35 @@ +# Default state for all rules +default: true + +# MD012/no-multiple-blanks - Multiple consecutive blank lines +MD012: + # Consecutive blank lines + maximum: 3 + +# MD013/line-length - Line length +MD013: + # Number of characters + line_length: 80 + # Number of characters for headings + heading_line_length: 80 + # Number of characters for code blocks + code_block_line_length: 80 + # Include code blocks + code_blocks: true + # Include tables + tables: true + # Include headings + headings: true + # Include headings + headers: true + # Strict length checking + strict: true + # Stern length checking + stern: false + +# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines +MD022: + # Blank lines above heading + lines_above: 2 + # Blank lines below heading + lines_below: 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..20825cb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Tronje Krop + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANUAL.md b/MANUAL.md new file mode 100644 index 0000000..f0239a5 --- /dev/null +++ b/MANUAL.md @@ -0,0 +1,404 @@ +# `go-make` Manual + + +## Setup and customization + +The [Makefile](Makefile) is using sensitive defaults that are supposed to work +out-of-the-box for most targets. Please see documentation of the target groups +for more information on setup and customization: + +* [Standard targets](#standard-targets) +* [Test targets](#test-targets) +* [Linter targets](#linter-targets) +* [Build targets](#install-targets) +* [Image targets](#image-targets) +* [Release targets](#run-targets) +* [Install targets](#install-targets) +* [Uninstall targets](#uninstall-targets) +* [Release targets](#release-targets) +* [Update targets](#update-targets) +* [Cleanup targets](#cleanup-targets) +* [Init targets](#init-targets) (usually no need to call) + +To customize the behavior of the Makefile there exist multiple extension points +that can be used to setup additional variables, definitions, and targets that +modify the behavior of the [Makefile](Makefile). + +* [Makefile.vars](Makefile.vars) allows to modify the behavior of standard + targets by customizing and defining additional variables (see section + [Modifying variables](#modifying-variables) for more details). +* [Makefile.defs](Makefile.defs) allows to customize the runtime environment + for executing of commands (see Section [Running commands](#running-commands) + for more details). +* [Makefile.targets](Makefile.targets) is an optional extension point that + allows to define arbitrary custom targets. + + +### Modifying variables + +While there exist sensible defaults for all configurations variables, some of +them might need to be adjusted. The following list provides an overview of the +most prominent ones + +```Makefile +# Setup code quality level (default: base). +CODE_QUALITY := plus +# Setup codacy integration (default: enabled [enabled, disabled]). +CODACY := enabled + +# Setup required targets before testing (default: ). +TEST_DEPS := run-db +# Setup required targets before running commands (default: ). +RUN_DEPS := run-db +# Setup required aws services for testing (default: ). +AWS_SERVICES := + +# Setup when to push images (default: pulls [never, pulls, merges]) +IMAGE_PUSH ?= never + +# Setup default test timeout (default: 10s). +TEST_TIMEOUT := 15s + +# Setup custom delivery file (default: delivery.yaml). +FILE_DELIVERY := delivery-template.yaml +# Setup custom local build targets (default: init test lint build). +TARGETS_ALL := init delivery test lint build + +# Custom linters applied to prepare next level (default: ). +LINTERS_CUSTOM := nonamedreturns gochecknoinits tagliatelle +# Linters swithed off to complete next level (default: ). +LINTERS_DISABLED := +``` + +You can easily lookup a list using `grep -r " ?= " Makefile`, however, most +will not be officially supported unless mentioned in the above list. + + +### Running commands + +To `run-*` commands as expected, you need to setup the environment variables +for your designated runtime by defining the custom functions for setting it up +via `run-setup`, `run-vars`, `run-vars-local`, and `run-vars-image` in +[Makefile.defs](Makefile.defs). + +While tests are supposed to run with global defaults and test specific config, +the setup of the `run-*` commands strongly depends on the commands execution +context and its purpose. Still, there are common patterns that can be copied +from other commands and projects. + +To enable postgres database support you must add `run-db` to `TEST_DEPS` and +`RUN_DEPS` variables to [Makefile.vars](Makefile.vars). + +You can also override the default setup via the `DB_HOST`, `DB_PORT`, `DB_NAME`, +`DB_USER`, and `DB_PASSWORD` variables, but this is optional. + +**Note:** when running test against a DB you usually have to extend the default +`TEST_TIMEOUT` of 10s to a less aggressive value. + +To enable AWS localstack you have to add `run-aws` to the default`TEST_DEPS` and +`RUN_DEPS` variables, as well as to add your list of required aws services to +the `AWS_SERVICES` variable. + +```Makefile +# Setup required targets before testing (default: ). +TEST_DEPS := run-aws +# Setup required targets before running commands (default: ). +RUN_DEPS := run-aws +# Setup required aws services for testing (default: ). +AWS_SERVICES := s3 sns +``` + +**Note:** Currently, the [Makefile](Makefile) does not support all command-line +arguments since make swallows arguments starting with `-`. To compensate this +shortcoming the commands need to support setup via command specific environment +variables following the principles of the [Twelf Factor App][12factor]. + +[12factor]: https://12factor.net/ + + +## Standard targets + +The [Makefile](Makefile) supports the following often used standard targets. + +```bash +make all # short cut target to init, test, and build binaries locally +make all-clean # short cut target to clean, init, test, and build binaries +make commit # short cut target to execute pr-commit lint and test steps +``` + +The short cut targets can be customized by setting up the variables `TARGETS_*` +(in upper letters), according to your preferences in `Makefile.vars` or in your ++environment. + +Other less customizable commands are targets to build, install, delete, and +cleanup project resources: + +```bash +make test # short cut to execute default test targets +make lint # short cut to execute default lint targets +make build # creates binary files of commands +make clean # removes all resource created during build +``` + +While these targets allow to execute the most important tasks out-of-the-box, +there exist a high number of specialized (sometimes project specific) commands +that provide more features with quicker response times for building, testing, +releasing, and executing of components. + +**Note:** All targets automatically trigger their preconditions and install the +latest version of the required tools, if some are missing. To enforce the setup +of a new tool, you need to run `make init` explicitly. + + +### Test targets + +Often it is more efficient or even necessary to execute the fine grained test +targets to complete a task. + +```bash +make test # short cut to execute default test targets +make test-all # executes the complete tests suite +make test-unit # executes only unit tests by setting the short flag +make test-self # executes a self-test of the build scripts +make test-cover # opens the test coverage report in the browser +make test-upload # uploads the test coverage files +make test-clean # cleans up the test files +make test-go # test go versions +``` + +In addition, it is possible to restrict test target execution to packages, +files and test cases as follows: + +* For a single package use `make test-(unit|all) ...`. +* For a single test file `make test[-(unit|all) /_test.go ...`. +* For a single test case `make test[-(unit|all) / ...`. + +The default test target can be customized by defining the `TARGETS_TEST` +variable in `Makefile.vars`. Usually this is not necessary. + + +### Linter targets + +The [Makefile](Makefile) supports different targets that help with linting +according to different quality levels, i.e. `min`,`base` (default), `plus`, +`max`, (and `all`) as well as automatically fixing the issues. + +```bash +make lint # short cut to execute default lint targets +make lint-min # lints the go-code using a minimal config +make lint-base # lints the go-code using a baseline config +make lint-plus # lints the go-code using an advanced config +make lint-max # lints the go-code using an expert config +make lint-all # lints the go-code using an insane all-in config +make lint-codacy # lints the go-code using codacy client side tools +make lint-markdown # lints the documentation using markdownlint +make lint-api # lints the api specifications in '/zalando-apis' +``` + +The default target for `make lint` is determined by the selected `CODE_QUALITY` +level (`min`, `base`, `plus`, and `max`), and the `CODACY` setup (`enabled`, +`disabled`). The default setup is to run the targets `lint-base`, `lint-apis`, +`lint-markdown`, and `lint-codacy`. It can be further customized via changing +the `TARGETS_LINT` in `Makefile.vars` - if necessary. + +The `lint-*` targets for `golangci-lint` allow some command line arguments: + +1. The keyword `fix` to lint with auto fixing enabled (when supported), +2. The keyword `config` to shows the effective linter configuration, +3. The keyword `linters` to display the linters with description, or +4. `,...` comma separated list of linters to enable for a quick checks. + +The default linter config is providing a golden path with different levels +out-of-the-box, i.e. a `min` for legacy code, `base` as standard for active +projects, and `plus` for experts and new projects, and `max` enabling all +but the conflicting disabled linters. Besides, there is an `all` level that +allows to experience the full linting capability. + +Independent of the golden path this setting provides, the lint expert levels +can be customized in three ways. + +1. The default way to customize linters is adding and removing linters for all + levels by setting the `LINTERS_CUSTOM` and `LINTERS_DISABLED` variables + providing a white space separated list of linters. +2. Less comfortable and a bit trickier is the approach to override the linter + config variables `LINTERS_DISCOURAGED`, `LINTERS_DEFAULT`, `LINTERS_MINIMUM`, + `LINTERS_BASELINE`, and `LINTERS_EXPERT`, to change the standards. +3. Last the linter configs can be changed via `.golangci.yaml`, as well as + via `.codacy.yaml`, `.markdownlint.yaml`, and `revive.toml`. + +However, customizing `.golangci.yaml` and other config files is currently not +advised, since the `Makefile` is designed to update and enforce a common +version of all configs on running `update-*` targets. + + +### Build targets + +The build targets can build native as well as linux platform executables using +the default system architecture. + +```bash +make build # builds default executables (native) +make build-native # builds native executables using system architecture +make build-linux # builds linux executable using the default architecture +make build-image # builds container image (alias for image-build) +``` + +The platform and architecture of the created executables can be customized via +`BUILDOS` and `BUILDARCH` environment variables. + + +### Image targets + +Based on the convention that all binaries are installed in a single container +image, the [Makefile](Makefile) supports to create and push the container image +as required for a pipeline. + +```bash +make image # short cut for 'image-build' +make image-build # build a container image after building the commands +make image-push # pushes a container image after building it +``` + +The targets are checking silently whether there is an image at all, and whether +it should be build and pushed according to the pipeline setup. You can control +this behavior by setting `IMAGE_PUSH` to `never` or `test` to disable pushing +(and building) or enable it in addition for pull requests. Any other value will +ensure that images are only pushed for `main`-branch and local builds. + + +### Run targets + +The [Makefile](Makefile) supports targets to startup a common DB and a common +AWS container image as well as to run the commands provided by the repository. + +```bash +make run-db # runs a postgres container image to provide a DBMS +make run-aws # runs a localstack container image to simulate AWS +make run-* # runs the matched command using its before build binary +make run-go-* # runs the matched command using 'go run' +make run-image-* # runs the matched command in the container image +``` + +To run commands successfully the environment needs to be setup to run the +commands in its runtim. Please visit [Running commands](#running-commands) for +more details on how to do this. + +**Note:** The DB (postgres) and AWS (localstack) containers can be used to +support any number of parallel applications, if they use different tables, +queues, and buckets. Developers are encouraged to continue with this approach +and only switch application ports and setups manually when necessary. + + +### Update targets + +The [Makefile](Makefile) supports targets for common update tasks for package +versions, for build, test, and linter tools, and for configuration files. + +```bash +make update # short cut for 'update-{go,deps,make}' +make update-all # short cut to execute all update targets +make update-go # updates the go version to the current compiler version +make update-deps # updates the project dependencies to the latest version +make update-tools # updates the project tools to the latest versions +make update-make # updates the the build environment to the latest version +``` + +Many update targets support a version with `?`-suffix to test whether an +update is available instead of executing it directly. In addition, a `major` +command line option can be used to also apply major version upgrades. + + +### Cleanup targets + +The [Makefile](Makefile) is designed to clean up everything it has created by +executing the following targets. + +```bash +make clean # short cut for clean-init, clean-build +make clean-all # cleans up all resources, i.e. also tools installed +make clean-init # cleans up all resources created by init targets +make clean-build # cleans up all resources created by build targets +make clean-run # cleans up all running container images +make clean-run-* # cleans up matched running container image +``` + + +### Install targets + +The install targets installs the latest build version of a command in the +`${GOPATH}/bin` directory for global command line execution. Usually commands +used by the project are installed automatically. + +```bash +make install # installs all software created by this project +make install-all # installs all software created by this project +make install-* # installs the matched software command or service +``` + +If a command, service, job has not been build before, it is first build. + +**Note:** Please use carefully, if your project uses common command names. + + +### Uninstall targets + +The uninstall targets remove the latest installed command from `${GOPATH}/bin`. +A full uninstall of commands used by the project can also be triggered by +`clean-all`. + +```bash +make uninstall # uninstalls all software created by this project +make uninstall-all # uninstalls all software created or used by this project +make uninstall-* # uninstalls the matched software command or service +``` + +**Note:** Please use carefully, if your project uses common command names. + + +### Release targets + +Finally, the [Makefile](Makefile) supports targets for releasing the provided +packages as library. + +```bash +make bump # bumps version to prepare a new release +make release # creates the release tags in the repository +``` + + +### Init targets + +The [Makefile](Makefile) supports initialization targets that are added as +perquisites for targets that require them. So there is usually no need to call +them manually. + + +```bash +make init # short cut for 'init-tools init-hooks init-packages' +make init-codacy # initializes the tools for running the codacy targets +make init-hooks # initializes github hooks for pre-commit, etc +make init-packages # initializes and downloads packages dependencies +make init-sources # initializes sources by generating mocks, etc +``` + + +## Compatibility + +This [Makefile](Makefile) is making extensive use of GNU tools but is supposed +to be compatible to all recent Linux and MacOS versions. Since MacOS is usually +a couple of years behind in applying the GNU standard tools, we document the +restrictions this creates here. + + +### `sed` in place substitution + +In MacOS we need to add `-e ''` after `sed -i` since else the command +section is not automatically restricted to a single argument. In linux this +restriction is automatically applied to the first argument. + + +### `realpath` not supported + +In MacOS we need to use `readlink -f` instead of `realpath`, since there may +not even be a simplified fallback of this command available. This is not the +preferred command in Linux, but for compatibility would be still acceptable. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d9eaf7a --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +SHELL := /bin/bash + +GOBIN ?= $(shell go env GOPATH)/bin +GOMAKE := github.com/tkrop/go-make@latest +TARGETS := $(shell command -v go-make >/dev/null || \ + go install $(GOMAKE) && go-make targets) + + +# Include custom variables to modify behavior. +ifneq ("$(wildcard Makefile.vars)","") + include Makefile.vars +else + $(warning warning: please customize variables in Makefile.vars) +endif + + +# Include standard targets from go-make providing group targets as well as +# single target targets. The group target is used to delegate the remaining +# request targets, while the single target can be used to define the +# precondition of custom target. +.PHONY: $(TARGETS) $(addprefix target/,$(TARGETS)) +$(TARGETS):; $(GOBIN)/go-make $(MAKEFLAGS) $(MAKECMDGOALS); +$(addprefix target/,$(TARGETS)): target/%: + $(GOBIN)/go-make $(MAKEFLAGS) $*; + + +# Include custom targets to extend scripts. +ifneq ("$(wildcard Makefile.ext)","") + include Makefile.ext +endif diff --git a/Makefile.base b/Makefile.base new file mode 100644 index 0000000..8fee0cd --- /dev/null +++ b/Makefile.base @@ -0,0 +1,1287 @@ +# === do not change this Makefile! === +# See MAKEFILE.md for documentation. + +SHELL := /bin/bash + +DIR_RUN := $(CURDIR)/run +DIR_CRED := $(DIR_RUN)/creds +DIR_BUILD := $(CURDIR)/build +DIR_TOOLS := $(DIR_BUILD)/tools +DIR_CONFIG := $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +FIND := find . ! -path "./run/*" ! -path "./build/*" +FILTER := sed "s/^.\///" | grep -E "^[a-z0-9/_-]*\.go$$" + +DIR_CONFIG := $(dir $(realpath $(firstword Makefile))) + +# Include custom variables to modify behavior. +ifneq ("$(wildcard Makefile.vars)","") + include Makefile.vars +else + $(warning info: please customize variables in Makefile.vars) +endif + +# Setup sensible defaults for configuration variables. +CODE_QUALITY ?= base +TEST_TIMEOUT ?= 10s + +# Function to search default config files +search-default = $(firstword $(wildcard $(1)) $(wildcard $(DIR_CONFIG)/$(1))) + +FILE_GOLANGCI ?= $(call search-default,.golangci.yaml) +FILE_MARKDOWN ?= $(call search-default,.markdownlint.yaml) +FILE_GITLEAKS ?= $(call search-default,.gitleaks.toml) +FILE_REVIVE ?= $(call search-default,revive.toml) +FILE_CODACY ?= $(call search-default,.codacy.yaml) + +FILE_CONTAINER ?= Dockerfile +FILE_DELIVERY ?= delivery.yaml +FILE_DELIVERY_REGEX ?= (cdp-runtime\/go-|go-version: \^?)[0-9.]* + +REPOSITORY ?= $(shell git remote get-url origin | \ + sed "s/^https:\/\///; s/^git@//; s/.git$$//; s/:/\//") +GITHOSTNAME ?= $(word 1,$(subst /, ,$(REPOSITORY))) +GITORGNAME ?= $(word 2,$(subst /, ,$(REPOSITORY))) +GITREPONAME ?= $(word 3,$(subst /, ,$(REPOSITORY))) + +TEAM ?= $(shell cat .zappr.yaml | grep "X-Zalando-Team" | \ + sed "s/.*:[[:space:]]*\([a-z-]*\).*/\1/") + + +TOOLS_NPM := $(TOOLS_NPM) \ + markdownlint-cli +TOOLS_GO := $(TOOLS_GO) \ + github.com/golangci/golangci-lint/cmd/golangci-lint \ + github.com/zalando/zally/cli/zally \ + golang.org/x/vuln/cmd/govulncheck \ + github.com/uudashr/gocognit/cmd/gocognit \ + github.com/fzipp/gocyclo/cmd/gocyclo \ + github.com/mgechev/revive@v1.2.3 \ + github.com/securego/gosec/v2/cmd/gosec \ + github.com/tsenart/deadcode \ + honnef.co/go/tools/cmd/staticcheck \ + github.com/zricethezav/gitleaks/v8 \ + github.com/icholy/gomajor \ + github.com/golang/mock/mockgen \ + github.com/tkrop/go-testing/cmd/mock \ + github.com/tkrop/go-make +TOOLS_SH := $(TOOLS_SH) \ + github.com/anchore/syft \ + github.com/anchore/grype + +# function for correct gol-package handling. +go-pkg = $(shell awk -v mode="$(1)" -v filter="$(3)" -v not="$(4)" ' \ + BEGIN { FS = "[/@]"; RS = "[ \n\r]" } { \ + field = NF; \ + if (version = index($$0, "@")) { field-- } \ + if ($$(field) ~ "v[0-9]+") { field-- } \ + if (!filter || \ + (not && ($$(field) !~ filter)) || \ + (!not && ($$(field) ~ filter))) { \ + if (mode == "cmd") { \ + if (!($$(field) in map)) { \ + print $$(field); map[$$(field)]++ \ + } \ + } else if (mode == "install" || mode == "update") { \ + if (!((package = strip(version)) in map)) { \ + if (version) { print $$0; } else { print $$0 "@latest" } \ + map[package]++ \ + } \ + } else if (mode == "strip") { \ + if (!((package = strip(version)) in map)) { \ + print package; map[package]++ \ + } \ + } \ + } \ + } \ + function strip(version) { \ + if (version) { \ + return substr($$0, 1, index($$0, "@") - 1) \ + } else { return $$0 } \ + }' <<<"$(2)") + + +VERSION ?= snapshot +ifeq ($(wildcard VERSION),VERSION) + BUILD_VERSION := $(cat VERSION) +else + BUILD_VERSION ?= $(VERSION) +endif + +IMAGE_PUSH ?= pulls +IMAGE_VERSION ?= $(VERSION) + +ifeq ($(words $(subst /, ,$(IMAGE_NAME))),3) + IMAGE_HOST ?= $(word 1,$(subst /, ,$(IMAGE_NAME))) + IMAGE_TEAM ?= $(word 2,$(subst /, ,$(IMAGE_NAME))) + IMAGE_ARTIFACT ?= $(word 3,$(subst /, ,$(IMAGE_NAME))) +else + IMAGE_HOST ?= pierone.stups.zalan.do + IMAGE_TEAM ?= $(TEAM) + IMAGE_ARTIFACT ?= $(GITREPONAME) +endif +IMAGE ?= $(IMAGE_HOST)/$(IMAGE_TEAM)/$(IMAGE_ARTIFACT):$(IMAGE_VERSION) + + +DB_HOST ?= 127.0.0.1 +DB_PORT ?= 5432 +DB_NAME ?= db +DB_USER ?= user +DB_PASSWORD ?= pass +DB_VERSION ?= latest +DB_IMAGE ?= postgres:$(DB_VERSION) + +AWS_SERVICES ?= sqs s3 +AWS_VERSION ?= latest +AWS_IMAGE ?= localstack/localstack:$(AWS_VERSION) + +# Setup codacy integration. +CODACY ?= enabled +ifdef CDP_PULL_REQUEST_NUMBER + CODACY_CONTINUE ?= true +else + CODACY_CONTINUE ?= false +endif +CODACY_PROVIDER ?= ghe +CODACY_USER ?= $(GITORGNAME) +CODACY_PROJECT ?= $(GITREPONAME) +CODACY_API_BASE_URL ?= https://codacy.bus.zalan.do +CODACY_CLIENTS ?= aligncheck deadcode +CODACY_BINARIES ?= gosec staticcheck +CODACY_GOSEC_VERSION ?= 0.4.5 +CODACY_STATICCHECK_VERSION ?= 3.0.12 + +# Function for conversion of variables name. +upper = $(shell echo "$(1)" | tr '[:lower:]' '[:upper:]') + + +# Default target list for all and cdp builds. +TARGETS_ALL ?= init test lint build image +TARGETS_INIT ?= init-hooks init-go $(if $(filter $(CODACY),enabled),init-codacy,) +TARGETS_CLEAN ?= clean-build +TARGETS_UPDATE ?= update-go update-deps update-make +TARGETS_COMMIT ?= test-go test-unit lint-leaks? lint-$(CODE_QUALITY) lint-markdown +TARGETS_TEST ?= test-all $(if $(filter $(CODACY),enabled),test-upload,) +TARGETS_LINT ?= lint-leaks? lint-$(CODE_QUALITY) lint-markdown lint-apis \ + $(if $(filter $(CODACY),enabled),lint-codacy,) + +UPDATE_MAKE ?= $(FILE_GOLANGCI) $(FILE_MARKDOWN) $(FILE_GITLEAKS) \ + $(FILE_CODACY) $(FILE_REVIVE) + +# Setup go to use desired and consistent go versions. +GOVERSION := $(shell go version | sed -E "s/.*go([0-9]+\.[0-9]+).*/\1/") + +# Export private repositories not to be downloaded. +export GOPRIVATE := github.bus.zalan.do +export GOBIN ?= $(shell go env GOPATH)/bin + + +# General setup of tokens for run-targets (not to be modified) + +# Often used token setup functions. +run-token-create = \ + ztoken > $(DIR_CRED)/token; echo "Bearer" > $(DIR_CRED)/type +run-token-link = \ + test -n "$(1)" && test -n "$(2)" && test -n "$(3)" && ( \ + test -h "$(DIR_CRED)/$(1)-$(2)" || ln -s type "$(DIR_CRED)/$(1)-$(2)" && \ + test -h "$(DIR_CRED)/$(1)-$(3)" || ln -s token "$(DIR_CRED)/$(1)-$(3)" \ + ) || test -n "$(1)" && test -n "$(2)" && test -z "$(3)" && ( \ + test -h "$(DIR_CRED)/$(1)" || ln -s type "$(DIR_CRED)/$(1)" && \ + test -h "$(DIR_CRED)/$(2)" || ln -s token "$(DIR_CRED)/$(2)" \ + ) || test -n "$(1)" && test -z "$(2)" && test -z "$(3)" && ( \ + test -h "$(DIR_CRED)/$(1)" || ln -s token "$(DIR_CRED)/$(1)" \ + ) || true + +# Stub function for general setup in run-targets. +run-setup = true +# Stub function for common variables in run-targets. +run-vars = +# Stub function for local runtime variables in run-targets. +run-vars-local = +# Stub function for container specific runtime variables in run-targets. +run-vars-image = $(call run-vars-docker) +# Stub function to setup aws localstack run-target. +run-aws-setup = true + +ifneq ("$(wildcard Makefile.defs)","") + $(info info: please define custom functions in Makefile.vars) + include Makefile.defs +endif + + +# Setup default environment variables. +SOURCES := $(shell $(FIND) -name "*.go" ! -name "mock_*_test.go" | $(FILTER)) +MODULES := $(shell echo $(SOURCES) | xargs -r dirname | sort -u) +COMMANDS := $(shell $(FIND) -name "main.go" | xargs -r readlink -f | \ + sed -E "s/^.*\/([^/]*)\/main.go$$/\1/" | sort -u) +COMMANDS_PKG := $(shell $(FIND) -name "main.go" | xargs -r readlink -f | \ + sed -E "s/^(.*\/([^/]*))\/main.go$$/\2=\1/; s|$(CURDIR)|.|") +COMMANDS_REGEX := $(shell echo "^$(COMMANDS)$$" | tr ' ' '|' ) +COMMANDS_GO := $(call go-pkg,cmd,$(TOOLS_GO),$(COMMANDS_REGEX),not) +COMMANDS_SH := $(call go-pkg,cmd,$(TOOLS_SH),$(COMMANDS_REGEX),not) + + +# Setup optimized golang mock setup environment. +MOCK_MATCH_DST := ^(.*)\/(.*):\/\/go:generate.*-destination=([^ ]*).*$$ +MOCK_MATCH_SRC := ^(.*)\/(.*):\/\/go:generate.*-source=([^ ]*).*$$ +MOCK_TARGETS := $(shell grep "//go:generate[[:space:]]*mockgen" $(SOURCES) | \ + sed -E "s/$(MOCK_MATCH_DST)/\1\/\3=\1\/\2/" | sort -u) +MOCK_SOURCES := $(shell grep "//go:generate[[:space:]]*mockgen.*-source" $(SOURCES) | \ + sed -E "s/$(MOCK_MATCH_SRC)/\1\/\3/" | sort -u | \ + xargs -r readlink -f | sed "s|$(PWD)/||g") +MOCKS := $(shell for TARGET in $(MOCK_TARGETS); \ + do echo "$${TARGET%%=*}"; done | sort -u) + +# Prepare phony make targets lists. +TARGETS_PUBLISH := $(addprefix publish-, all $(COMMANDS)) +TARGETS_TEST_INIT := test-clean init-sources init-hooks +TARGETS_INIT_CODACY := $(addprefix init-, $(CODACY_BINARIES)) +TARGETS_LINT_CODACY_CLIENTS := $(addprefix lint-, $(CODACY_CLIENTS)) +TARGETS_LINT_CODACY_BINARIES := $(addprefix lint-, $(CODACY_BINARIES)) +TARGETS_IMAGE := $(shell $(FIND) -type f -name "$(FILE_CONTAINER)*" | sed 's|^./||') +TARGETS_IMAGE_BUILD := $(addprefix image-build/, $(TARGETS_IMAGE)) +TARGETS_IMAGE_PUSH := $(addprefix image-push/, $(TARGETS_IMAGE)) +TARGETS_BUILD := $(addprefix build-, $(COMMANDS)) +TARGETS_BUILD_LINUX := $(addprefix $(DIR_BUILD)/linux/, $(COMMANDS)) +TARGETS_INSTALL := $(addprefix install-, $(COMMANDS)) +TARGETS_INSTALL_GO := $(addprefix install-, $(COMMANDS_GO)) +TARGETS_INSTALL_SH := $(addprefix install-, $(COMMANDS_SH)) +TARGETS_INSTALL_NPM := $(addprefix install-, $(TOOLS_NPM:-cli=)) +TARGETS_INSTALL_ALL := $(TARGETS_INSTALL) $(TARGETS_INSTALL_GO) $(TARGETS_INSTALL_NPM) +TARGETS_UNINSTALL := $(addprefix uninstall-, $(COMMANDS)) +TARGETS_UNINSTALL_GO := $(addprefix uninstall-, $(COMMANDS_GO)) +TARGETS_UNINSTALL_SH := $(addprefix uninstall-, $(COMMANDS_SH)) +TARGETS_UNINSTALL_NPM := $(addprefix uninstall-, $(TOOLS_NPM:-cli=)) +TARGETS_UNINSTALL_CODACY := $(addprefix uninstall-codacy-, $(CODACY_BINARIES)) +TARGETS_UNINSTALL_ALL := $(TARGETS_UNINSTALL) $(TARGETS_UNINSTALL_GO) \ + $(TARGETS_UNINSTALL_NPM) $(TARGETS_UNINSTALL_CODACY) +TARGETS_RUN := $(addprefix run-, $(COMMANDS)) +TARGETS_RUN_GO := $(addprefix run-go-, $(COMMANDS)) +TARGETS_RUN_IMAGE := $(addprefix run-image-, $(COMMANDS)) +TARGETS_RUN_CLEAN := $(addprefix run-clean-, $(COMMANDS) db aws) +TARGETS_CLEAN_ALL := clean-init $(TARGETS_CLEAN) clean-run $(TARGETS_UNINSTALL_ALL) +TARGETS_CLEAN_RUN := $(addprefix clean-run-, $(COMMANDS) db aws) +TARGETS_UPDATE_GO := $(addprefix update-,$(COMMANDS_GO)) +TARGETS_UPDATE_MAKE := $(addprefix update/,$(UPDATE_MAKE)) +TARGETS_UPDATE_MAKE? := $(addsuffix ?,$(TARGETS_UPDATE_MAKE)) +TARGETS_UPDATE_ALL := update-go update-deps update-tools update-make +TARGETS_UPDATE_ALL? := udate-go? update-deps? update-make? +TARGETS_UPDATE? := $(filter $(addsuffix ?,$(TARGETS_UPDATE)), \ + $(TARGETS_UPDATE_ALL?) $(TARGETS_UPDATE_MAKE?)) + +# Setup phony make targets to always be executed. +.PHONY: all all-clean help list commit bump release publish +.PHONY: init init-all init-go init-hooks init-sources +.PHONY: init-codacy $(TARGETS_INIT_CODACY) +.PHONY: test test-all test-unit test-bench test-clean test-cover +.PHONY: test-upload test-go +.PHONY: lint lint-min lint-base lint-plus lint-max lint-all lint-config +.PHONY: lint-leaks lint-leaks? lint-vuln lint-gocognit lint-gocyclo +.PHONY: lint-markdown lint-apis lint-codacy lint-revive +.PHONY: $(TARGETS_LINT_CODACY_CLIENTS) $(TARGETS_LINT_CODACY_BINARIES) +.PHONY: build build-native build-linux build-image $(TARGETS_BUILD) +.PHONY: image image-build $(TARGETS_IMAGE_BUILD) +.PHONY: image-push $(TARGETS_IMAGE_PUSH) +.PHONY: install install-all $(TARGETS_INSTALL_ALL) +.PHONY: uninstall uninstall-all $(TARGETS_UNINSTALL_ALL) +.PHONY: run-native run-image run-clean $(TARGETS_RUN) run-db run-aws +.PHONY: $(TARGETS_RUN_GO) $(TARGETS_RUN_IMAGE) $(TARGETS_RUN_CLEAN) +.PHONY: clean clean-all $(TARGETS_CLEAN) +.PHONY: $(TARGETS_CLEAN_ALL) $(TARGETS_CLEAN_RUN) +.PHONY: update update? update-all update-all? +.PHONY: $(TARGETS_UPDATE_ALL) $(TARGETS_UPDATE_ALL?) $(TARGETS_UPDATE_GO) +.PHONY: $(TARGETS_UPDATE_MAKE) $(TARGETS_UPDATE_MAKE?) update-base + + +# Setup docker or podman command. +IMAGE_CMD ?= $(shell command -v docker || command -v podman) +ifndef IMAGE_CMD + $(error error: docker/podman command not found) +endif + + +# Helper function to resolve match position of word in text. +findany = $(strip $(foreach word,$(1),$(findstring $(word),$(2)))) +pos-recurs = $(if $(findstring $(1),$(2)),$(call pos-recurs,$(1),\ + $(wordlist 2,$(words $(2)),$(2)),x $(3)),$(3)) +pos = $(words $(call pos-recurs,$(1),$(2))) + +# match commands that support arguments ... +CMDWORDS := targets bump release update test- lint run- +CMDMATCH = $(call findany,$(CMDWORDS),$(MAKECMDGOALS))% + +# If any argument contains "run*", "test*", "lint*", "bump" ... +ifneq ($(CMDMATCH),%) + CMD := $(filter $(CMDMATCH),$(MAKECMDGOALS)) + POS = $(call pos,$(CMD),$(MAKECMDGOALS)) + # ... then use the rest as arguments for "run/test-" ... + CMDARGS := $(wordlist $(shell expr $(POS) + 1),\ + $(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) + # ...and turn them into do-nothing targets. + $(eval $(CMDARGS):;@:) + RUNARGS ?= $(CMDARGS) + $(shell if [ -n "$(RUNARGS)" ]; then \ + echo "info: captured arguments [$(RUNARGS)]" >/dev/stderr; \ + fi) +endif + + +## Standard: default targets to test, lint, and build. + +#@ executes the default targets. +all: $(TARGETS_ALL) +#@ executes the default targets after cleaning up. +all-clean: clean clean-run all +#@ executes the pre-commit check targets. +commit: $(TARGETS_COMMIT) +$(DIR_BUILD) $(DIR_RUN) $(DIR_CRED): + @if [ ! -d "$@" ]; then mkdir -p $@; fi; + + +## Support: targets to support processes and users. + +#@ prints this help. +help: + @cat $(MAKEFILE_LIST) | awk ' \ + BEGIN { \ + printf("\n\033[1mUsage:\033[0m $(MAKE) \033[36m\033[0m\n") \ + } \ + /^## .*/ { \ + if (i = index($$0, ":")) { \ + printf("\n\033[1m%s\033[0m%s\n", \ + substr($$0, 4, i - 4), substr($$0, i)) \ + } else { \ + printf("\n\033[1m%s\033[0m\n", substr($$0, 4)) \ + } next \ + } \ + /^#@ / { \ + if (i = index($$0, ":")) { \ + printf(" \033[36m%-25s\033[0m %s\n", \ + substr($$0, 4, i - 4), substr($$0, i + 2)) \ + } else { line = substr($$0, 4) } next \ + } \ + /^[a-zA-Z_0-9?-]+:/ { \ + if (line) { \ + target = substr($$1, 1, length($$1) - 1); \ + if (i = index(line, " # ")) { \ + target = target " " substr(line, 1, i - 1); \ + line = substr(line, i + 3) \ + } \ + printf(" \033[36m%-25s\033[0m %s\n", target, line); \ + line = "" \ + } \ + }'; + +#@ show actual makefile. +show: + @$(MAKE) --no-builtin-rules --no-builtin-variables --print-data-base \ + --question --makefile=$(firstword $(MAKEFILE_LIST)) 2>/dev/null || true; +#@ # list all available targets. +targets: + @$(MAKE) --no-builtin-rules --no-builtin-variables --print-data-base \ + --question --makefile=$(firstword $(MAKEFILE_LIST)) 2>/dev/null | \ + if [ "$(RUNARGS)" != "raw" ]; then awk -v RS= -F: ' \ + /(^|\n)# Files(\n|$$)/,/(^|\n)# Finished Make data base/ { \ + if ($$1 !~ "^[#.]") { print $$1 } \ + }' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$'; \ + else cat -; fi || true; + +#@ # updates version and prepare release of the software. +bump: + @if [ -z "$(RUNARGS)" ]; then \ + echo "error: missing new version"; exit 1; \ + elif [[ "$(RUNARGS)" =~ "^[+~].*$$" ]]; then \ + VERSION="$$(awk -v op=$(RUNARGS) '{ \ + patsplit($$0, vers, "[0-9]*", seps); len = length(seps); \ + if (op ~ "+.*") { vers[length(op)]++ } else { vers[length(op)]-- } \ + for (i = 1; i < len; i++) { printf("%d%s", vers[i], seps[i]) } \ + }' VERSION)"; \ + elif ! [[ "$(RUNARGS)" =~ "^[0-9]+(\.[0-9]+){0,2}(-.*)?$$" ]]; then \ + echo "error: invalid new version [$(RUNARGS)]"; exit 1; \ + else VERSION="$(RUNARGS)"; fi; echo $${VERSION} >VERSION; \ + echo "Bumped version to $${VERSION} for auto release!"; \ + + +#@ # releases a fixed version of the software as library. +release: + @if [ -f VERSION ]; then VERSION="$$(cat VERSION)"; fi; \ + if ! [[ "$(RUNARGS)" =~ "^[0-9]+(\.[0-9]+){0,2}(-.*)?$$" ]]; then \ + VERSION="$(RUNARGS)"; \ + fi; \ + if [ -n "$${VERSION}" -a -z "$$(git tag -l "v$${VERSION}")" ]; then \ + git gh-release "v$${VERSION}" || ( \ + git tag "v$${VERSION}" && git push origin "v$${VERSION}" \ + ) && echo "Added release tag v$${VERSION} to repository!"; \ + fi; \ + +publish: $(TARGETS_PUBLISH) +$(TARGETS_PUBLISH): publish-%: + @if [ "$*" != "all" ]; then \ + if [ -f "cmd/$*/main.go" ]; then CMD="/cmd/$*"; else exit 0; fi; \ + fi; \ + if [ -f VERSION ]; then VERSION="$$(cat VERSION)"; else \ + VERSION="$$(TZ=UTC0 git show --quiet --date='format-local:%Y%m%d%H%M%S' \ + --format="0.0.0-%cd-%H" | cut -c -33)"; \ + fi; \ + TARGET="$(REPOSITORY)$${CMD}@v$${VERSION}"; \ + if [ "$(GITHOSTNAME)" == "github.com" ]; then \ + if [ -z "$${CMD}" ]; then \ + echo "publish: curl $${TARGET}"; \ + curl --silent --show-error --fail --location \ + https://sum.golang.org/lookup/$${TARGET};\ + fi; \ + if [ -f ".$${CMD}/main.go" ]; then \ + echo "publish: go install $${TARGET}"; \ + go install $${TARGET}; \ + fi; \ + fi; + +#@ install all software created by the project. +install: $(TARGETS_INSTALL) +#@ install all software created and used by the project. +install-all: $(TARGETS_INSTALL_ALL) +#@ install-*: install the matched software command or service. + +# install go tools used by the project. +$(TARGETS_INSTALL_GO): install-%: $(GOBIN)/% +$(addprefix $(GOBIN)/,$(COMMANDS_GO)): $(GOBIN)/%: + go install $(call go-pkg,install,$(TOOLS_GO),^$*$$); +# install go tools providing an install.sh script. +$(TARGETS_INSTALL_SH): install-%: $(GOBIN)/% +$(addprefix $(GOBIN)/,$(COMMANDS_SH)): $(GOBIN)/%: + @if ! command -v $*; then \ + curl --silent --show-error --fail --location \ + https://raw.githubusercontent.com/anchore/$*/main/install.sh | \ + sh -s -- -b $(GOBIN); \ + fi; +# install npm tools used by the project. +$(TARGETS_INSTALL_NPM): install-%: $(NVM_BIN)/% +$(addprefix $(NVM_BIN)/,$(TOOLS_NPM:-cli=)): $(NVM_BIN)/%: + @if command -v npm &> /dev/null && ! command -v $*; then \ + echo "npm install --global ^$*$$"; \ + npm install --global $(filter $*-cli,$(TOOLS_NPM)); \ + fi; + +#@ install software command or service created by the project. +$(TARGETS_INSTALL): install-%: $(GOBIN)/% +$(addprefix $(GOBIN)/,$(COMMANDS)): $(GOBIN)/%: $(DIR_BUILD)/% + cp $< $(GOBIN)/$*; + +#@ uninstall all software created by the project. +uninstall: $(TARGETS_UNINSTALL) +#@ uninstall all software created and used by the projct. +uninstall-all: $(TARGETS_UNINSTALL_ALL) +#@ uninstall-*: uninstall the matched software command or service. + +# uninstall go tools used by the project. +$(TARGETS_UNINSTALL_GO): uninstall-%: + @#PACKAGE=$(call go-pkg,strip,$(TOOLS_GO),^$*$$); \ + rm -rf $(wildcard $(GOBIN)/$* $(GOBIN)/$*.config); +# uninstall npm based tools used by the project. +$(TARGETS_UNINSTALL_NPM): uninstall-%: + @if command -v npm &> /dev/null; then \ + echo "npm uninstall --global $*"; \ + npm uninstall --global $(filter $*-cli,$(TOOLS_NPM)); \ + fi; +# uninstall codacy tools used by the project. +$(TARGETS_UNINSTALL_CODACY): uninstall-codacy-%: + @VERSION="$(CODACY_$(call upper,$*)_VERSION)"; \ + rm -f "$(GOBIN)/codacy-$*-$${VERSION}"; +# uninstall software command or service created by the project. +$(TARGETS_UNINSTALL): uninstall-%:; rm -f $(GOBIN)/$*; + + +## Init: targets to initialize tools and (re-)sources. + +#@ initializes the project to prepare it for building. +init: $(TARGETS_INIT) +#@ initializes the go module and package dependencies. +init-go: go.mod go.sum + go mod download; +go.mod go.sum: + go mod init "$(REPOSITORY)" +#@ initializes the pre-commit hook. +init-hooks: .git/hooks/pre-commit +.git/hooks/pre-commit: + @echo -ne "#!/bin/sh\n$(MAKE) commit" >$@; chmod 755 $@; + +#@ initializes the generated sources. +init-sources: $(MOCKS) +$(MOCKS): go.sum $(MOCK_SOURCES) $(GOBIN)/mockgen $(GOBIN)/mock + go generate "$(shell echo $(MOCK_TARGETS) | \ + sed -E "s:.*$@=([^ ]*).*$$:\1:;")"; + +#@ initializes the codacy support - if enabled. +init-codacy: $(TARGETS_INIT_CODACY) +$(TARGETS_INIT_CODACY): init-%: + @VERSION="$(CODACY_$(call upper,$*)_VERSION)"; \ + FILE="$(GOBIN)/codacy-$*-$${VERSION}"; \ + if [ ! -f $${FILE} ]; then \ + BASE="https://github.com/codacy/codacy-$*/releases/download"; \ + echo curl --silent --location --output $${FILE} \ + $${BASE}/$${VERSION}/codacy-$*-$${VERSION}; \ + curl --silent --location --output $${FILE} \ + $${BASE}/$${VERSION}/codacy-$*-$${VERSION}; \ + chmod 700 $${FILE}; \ + fi; \ + + +## Test: targets to test the source code (and compiler environment). +TEST_ALL := $(DIR_BUILD)/test-all.cover +TEST_UNIT := $(DIR_BUILD)/test-unit.cover +TEST_BENCH := $(DIR_BUILD)/test-bench.cover + +#@ executes default test set. +test: $(TARGETS_TEST) +#@ [/] # executes all tests. +test-all: $(TARGETS_TEST_INIT) $(TEST_ALL) +#@ [/] # executes only unit tests. +test-unit: $(TARGETS_TEST_INIT) $(TEST_UNIT) +#@ [/] # executes benchmarks. +test-bench: $(TARGETS_TEST_INIT) $(TEST_BENCH) + +# removes all coverage files of test and benchmarks. +test-clean: + @if [ -f "$(TEST_ALL)" ]; then rm -vf $(TEST_ALL); fi; \ + if [ -f "$(TEST_UNIT)" ]; then rm -vf $(TEST_UNIT); fi; \ + if [ -f "$(TEST_BENCH)" ]; then rm -vf $(TEST_BENCH); fi; \ + +#@ starts the test coverage report. +test-cover: + @FILE=$$(ls -Art "$(TEST_ALL)" "$(TEST_UNIT)" \ + "$(TEST_BENCH)" 2>/dev/null); \ + go tool cover -html="$${FILE}"; \ + +#@ uploads the test coverage report to codacy. +test-upload: + @FILE=$$(ls -Art "$(TEST_ALL)" "$(TEST_UNIT)" \ + "$(TEST_BENCH)" 2>/dev/null); \ + COMMIT="$$(git log --max-count=1 --pretty=format:"%H")"; \ + SCRIPT="https://coverage.codacy.com/get.sh"; \ + if [ -n "$(CODACY_PROJECT_TOKEN)" ]; then \ + bash <(curl --silent --location $${SCRIPT}) report \ + --project-token $(CODACY_PROJECT_TOKEN) --commit-uuid $${COMMIT} \ + --codacy-api-base-url $(CODACY_API_BASE_URL) --language go \ + --force-coverage-parser go --coverage-reports "$${FILE}"; \ + elif [ -n "$(CODACY_API_TOKEN)" ]; then \ + bash <(curl --silent --location $${SCRIPT}) report \ + --organization-provider $(CODACY_PROVIDER) \ + --username $(CODACY_USER) --project-name $(CODACY_PROJECT) \ + --api-token $(CODACY_API_TOKEN) --commit-uuid $${COMMIT} \ + --codacy-api-base-url $(CODACY_API_BASE_URL) --language go \ + --force-coverage-parser go --coverage-reports "$${FILE}"; \ + fi; \ + +# Function to test versions. +test-go = \ + for VERSION in $${VERSIONS}; do \ + if [ "$(GOVERSION)" != "$${VERSION}" ]; then \ + echo "$${ERROR}: '$(1)' requires $${VERSION}!"; \ + if [[ "$(GOVERSION)" < "$${VERSION}" ]]; then \ + GOCHANGE="upgrade"; CHANGE="downgrade"; \ + else \ + GOCHANGE="downgrade"; CHANGE="upgrade"; \ + fi; \ + echo -e "\t$${GOCHANGE} your local go version to $${VERSION} or "; \ + echo -e "\trun 'make update-go $${VERSION}' to adjust the project!"; \ + exit -1; \ + fi; \ + done; + +#@ tests whether project is using the latest go-version. +test-go: + @ERROR="error: local go version is $(GOVERSION)"; \ + if [ ! -f go.mod ]; then \ + VERSIONS=$$(grep "^go [0-9.]*$$" go.mod | cut -f2 -d' '); \ + $(call test-go,go.mod) \ + fi; \ + if [ -f "$(FILE_DELIVERY)" ]; then \ + VERSIONS="$$(grep -Eo "$(FILE_DELIVERY_REGEX)" \ + "$(FILE_DELIVERY)" | grep -Eo "[0-9.]*" | sort -u)"; \ + $(call test-go,$(FILE_DELIVERY)) \ + fi; \ + +# process test arguments. +testargs = \ + if [ -n "$(RUNARGS)" ]; then ARGS=($(RUNARGS)); \ + if [[ -f "$${ARGS[0]}" && ! -d "$${ARGS[0]}" && \ + "$${ARGS[0]}" == *_test.go ]]; then \ + find $$(dirname $(RUNARGS) | sort -u) \ + -maxdepth 1 -a -name "*.go" -a ! -name "*_test.go" \ + -o -name "common_test.go" -o -name "mock_*_test.go" | \ + sed "s|^|./|"; \ + echo $(addprefix ./,$(RUNARGS)); \ + elif [[ -d "$${ARGS[0]}" && ! -f "$${ARGS[0]}" ]]; then \ + echo $(addprefix ./,$(RUNARGS)); \ + elif [[ ! -f "$${ARGS[0]}" && ! -d "$${ARGS[0]}" ]]; then \ + for ARG in $${ARGS[0]}; do \ + if [ -z "$${PACKAGES}" ]; then PACKAGES="$${ARG%/*}"; \ + else PACKAGES="$${PACKAGES}\n$${ARG%/*}"; fi; \ + if [ -z "$${TEST_CASE}" ]; then TEST_CASES="-run $${ARG\#\#*/}"; \ + else TEST_CASES="$${TEST_CASES} -run $${ARG\#\#*/}"; fi; \ + done; \ + echo -en "$${PACKAGES}" | sort -u | sed "s|^|./|"; \ + echo "$${TEST_CASES}"; \ + else \ + echo "warning: invalid test parameters [$${ARGS[@]}]"; \ + fi; \ + else echo "./..."; fi + +TEST_FLAGS ?= -race -mod=readonly -count=1 +TEST_ARGS ?= $(shell $(testargs)) + +# actuall targets for testing. +$(TEST_ALL): $(SOURCES) init-sources $(TEST_DEPS) $(DIR_BUILD) + go test $(TEST_FLAGS) -timeout $(TEST_TIMEOUT) \ + -cover -coverprofile $@ $(TEST_ARGS); +$(TEST_UNIT): $(SOURCES) init-sources $(DIR_BUILD) + go test $(TEST_FLAGS) -timeout $(TEST_TIMEOUT) \ + -cover -coverprofile $@ -short $(TEST_ARGS); +$(TEST_BENCH): $(SOURCES) init-sources $(DIR_BUILD) + go test $(TEST_FLAGS) -benchtime=8s \ + -cover -coverprofile $@ -short -bench=. $(TEST_ARGS); + +#@ executes a kind of self-test running the main targets. +test-self: clean-all all-clean clean-all + @echo "Self test finished successfully!!!"; + +# Variables and definitions for linting of source code. +COMMA := , +SPACE := $(null) # + +# disabled (deprecated): deadcode golint interfacer ifshort maligned musttag +# nosnakecase rowserrcheck scopelint structcheck varcheck wastedassign +# diabled (distructive): nlreturn ireturn nonamedreturns varnamelen exhaustruct +# exhaustivestruct gochecknoglobals gochecknoinits tagliatelle +# disabled (conflicting): godox gci paralleltest +# not listed (unnecessary): forcetypeassert wsl +# requires unknown setup: depguard + +LINTERS_DISCOURAGED ?= \ + deadcode exhaustivestruct exhaustruct gci gochecknoglobals gochecknoinits \ + godox golint ifshort interfacer ireturn maligned musttag nlreturn \ + nonamedreturns nosnakecase paralleltest rowserrcheck scopelint structcheck \ + tagliatelle varcheck varnamelen wastedassign +LINTERS_MINIMUM ?= \ + asasalint asciicheck bidichk bodyclose dogsled dupl dupword durationcheck \ + errchkjson execinquery exportloopref funlen gocognit goconst gocyclo \ + godot gofmt gofumpt goimports gosimple govet importas ineffassign maintidx \ + makezero misspell nestif nilerr prealloc predeclared promlinter reassign \ + sqlclosecheck staticcheck typecheck unconvert unparam unused \ + usestdlibvars whitespace +LINTERS_BASELINE ?= \ + containedctx contextcheck cyclop decorder errname forbidigo ginkgolinter \ + gocheckcompilerdirectives goheader gomodguard goprintffuncname gosec \ + grouper interfacebloat lll loggercheck nakedret nilnil noctx nolintlint \ + nosprintfhostport +LINTERS_EXPERT ?= \ + errcheck errorlint exhaustive gocritic goerr113 gomnd gomoddirectives \ + revive stylecheck tenv testableexamples testpackage thelper tparallel \ + wrapcheck + +LINT_FILTER := $(LINTERS_CUSTOM) $(LINTERS_DISABLED) +LINT_DISABLED ?= $(subst $(SPACE),$(COMMA),$(strip \ + $(filter-out $(LINT_FILTER),$(LINTERS_DISCOURAGED)) $(LINTERS_DISABLED))) +LINT_MINIMUM ?= $(subst $(SPACE),$(COMMA),$(strip \ + $(filter-out $(LINT_FILTER),$(LINTERS_MINIMUM)) $(LINTERS_CUSTOM))) +LINT_BASELINE ?= $(subst $(SPACE),$(COMMA),$(strip $(LINT_MINIMUM) \ + $(filter-out $(LINT_FILTER),$(LINTERS_BASELINE)))) +LINT_EXPERT ?= $(subst $(SPACE),$(COMMA),$(strip $(LINT_BASELINE) \ + $(filter-out $(LINT_FILTER),$(LINTERS_EXPERT)))) + +ifeq ($(shell ls $(FILE_GOLANGCI) 2>/dev/null), $(FILE_GOLANGCI)) + LINT_CONFIG := --config $(FILE_GOLANGCI) +endif + +LINT_MIN := --enable $(LINT_MINIMUM) --disable $(LINT_DISABLED) +LINT_BASE := --enable $(LINT_BASELINE) --disable $(LINT_DISABLED) +LINT_PLUS := --enable $(LINT_EXPERT) --disable $(LINT_DISABLED) +LINT_MAX := --enable-all --disable $(LINT_DISABLED) +LINT_ALL := --enable $(LINT_EXPERT),$(LINT_DISABLED) --disable-all + +LINT_FLAGS ?= --allow-serial-runners --sort-results --color always +LINT_CMD ?= golangci-lint run $(LINT_CONFIG) $(LINT_FLAGS) +LINT_CMD_CONFIG := make --no-print-directory lint-config +ifeq ($(RUNARGS),linters) + LINT_CMD := golangci-lint linters $(LINT_CONFIG) $(LINT_FLAGS) +else ifeq ($(RUNARGS),config) + LINT_CMD := @ + LINT_MIN := LINT_ENABLED=$(LINT_MINIMUM) \ + LINT_DISABLED=$(LINT_DISABLED) $(LINT_CMD_CONFIG) + LINT_BASE := LINT_ENABLED=$(LINT_BASELINE) \ + LINT_DISABLED=$(LINT_DISABLED) $(LINT_CMD_CONFIG) + LINT_PLUS := LINT_ENABLED=$(LINT_EXPERT) \ + LINT_DISABLED=$(LINT_DISABLED) $(LINT_CMD_CONFIG) + LINT_MAX := LINT_DISABLED=$(LINT_DISABLED) $(LINT_CMD_CONFIG) + LINT_ALL := LINT_ENABLED=$(LINT_EXPERT),$(LINT_DISABLED) $(LINT_CMD_CONFIG) +else ifeq ($(RUNARGS),fix) + LINT_CMD := golangci-lint run $(LINT_CONFIG) $(LINT_FLAGS) --fix +else ifneq ($(RUNARGS),) + LINT_CMD := golangci-lint run $(LINT_CONFIG) $(LINT_FLAGS) + LINT_MIN := --disable-all --enable $(RUNARGS) + LINT_BASE := --disable-all --enable $(RUNARGS) + LINT_PLUS := --disable-all --enable $(RUNARGS) + LINT_MAX := --disable-all --enable $(RUNARGS) + LINT_ALL := --disable-all --enable $(RUNARGS) +endif + + +## Lint: targets to lint source code. + +#@ # execute linters for custom code quality level. +lint: $(TARGETS_LINT) +#@ # execute golangci linters for minimal code quality level. +lint-min: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_MIN) +#@ # execute golangci linters for base code quality level. +lint-base: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_BASE) +#@ # execute golangci linters for plus code quality level. +lint-plus: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_PLUS) +#@ # execute golangci linters for maximal code quality level. +lint-max: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_MAX) +#@ # execute all golangci linters for insane code quality level. +lint-all: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_ALL) + +#@ creates a golangci linter config using the custom code quality level. +lint-config: + @LINT_START=$$(awk '($$0 == "linters:"){print NR-1}' $(FILE_GOLANGCI)); \ + LINT_STOP=$$(awk '(start && $$0 ~ "^[^ ]+:$$"){print NR; start=0} \ + ($$0 == "linters:"){start=1}' $(FILE_GOLANGCI)); \ + (sed -ne '1,'$${LINT_START}'p' $(FILE_GOLANGCI); \ + echo -e "linters:\n enable:"; \ + for LINTER in "# List of min-set linters (min)" \ + $(LINTERS_MINIMUM) \ + "# List of base-set linters (base)" $(LINTERS_BASELINE) \ + "# List of plus-set linters (plus)" $(LINTERS_EXPERT); do \ + if [ "$${LINTER:0:1}" == "#" ]; then X=""; else X="- "; fi; \ + echo -e " $${X}$${LINTER}"; \ + done; echo -e "\n disable:"; \ + for LINTER in "# List of to-avoid linters (avoid)" \ + $(LINTERS_DISABLED); do \ + if [ "$${LINTER:0:1}" == "#" ]; then X=""; else X="- "; fi; \ + echo -e " $${X}$${LINTER}"; \ + done; \ + echo; sed -ne $${LINT_STOP}',$$p' $(FILE_GOLANGCI)) | \ + awk -v enabled=$${LINT_ENABLED} -v disabled=$${LINT_DISABLED} ' \ + ((start == 2) && ($$1 == "-")) { \ + enabled = enable[$$2]; disabled = disable[$$2]; \ + if (enabled && disabled) { \ + print $$0 " # (conflicting)" \ + } else if (!enabled && disabled) { \ + print gensub("-","# -", 1) " # (disabled)" \ + } else if (!enabled && !disabled && enone) { \ + print gensub("-","# -", 1) " # (missing)" \ + } else { print $$0 } \ + enable[$$2] = 2; next \ + } \ + ((start == 3) && ($$1 == "-")) { \ + enabled = enable[$$2]; disabled = disable[$$2]; \ + if (enabled && disabled) { \ + print gensub("-","# -", 1) " # (conflicting)" \ + } else if (enabled && !disabled) { \ + print gensub("-","# -", 1) " # (enabled)" \ + } else if (!enabled && !disabled && dnone) { \ + print gensub("-","# -", 1) " # (missing)" \ + } else { print $$0 } \ + disable[$$2] = 2; next \ + } \ + ((start != 0) && ($$0 ~ "^[^ ]+:$$")) { start=0 } \ + ((start >= 2) && ($$0 ~ "^ [^ ]+:$$")) { none=1; \ + if (start == 2) { for (key in enable) { \ + if (key && enable[key] == 1) { \ + if (none) { print " # Additional linters" } \ + print " - " key; none=0 \ + } \ + } } else if (start == 3) { for (key in disable) { \ + if (key && disable[key] == 1) { \ + if (none) { print " # Additional linters" } \ + print " - " key; none=0 \ + } \ + } } \ + } \ + ((start != 0) && ($$0 == " disable:")) { start = 3 }\ + ((start != 0) && ($$0 == " enable:")) { start = 2 } \ + ((start == 0) && ($$0 == "linters:")) { start = 1; \ + enone = split(enabled, array, ","); \ + for (i in array){ enable[array[i]] = 1 } \ + dnone = split(disabled, array, ","); \ + for (i in array){ disable[array[i]] = 1 } \ + } { print $$0 }' \ + +#@ execute all codacy linters. +lint-codacy: lint-revive $(TARGETS_LINT_CODACY_CLIENTS) $(TARGETS_LINT_CODACY_BINARIES) +$(TARGETS_LINT_CODACY_CLIENTS): lint-%: init-sources + @LARGS=("--allow-network" "--skip-uncommitted-files-check"); \ + LARGS+=("--codacy-api-base-url" "$(CODACY_API_BASE_URL)"); \ + if [ -n "$(CODACY_PROJECT_TOKEN)" ]; then \ + LARGS+=("--project-token" "$(CODACY_PROJECT_TOKEN)"); \ + LARGS+=("--upload" "--verbose"); \ + elif [ -n "$(CODACY_API_TOKEN)" ]; then \ + LARGS+=("--provider" "$(CODACY_PROVIDER)"); \ + LARGS+=("--username" "$(CODACY_USER)"); \ + LARGS+=("--project" "$(CODACY_PROJECT)"); \ + LARGS+=("--api-token" "$(CODACY_API_TOKEN)"); \ + LARGS+=("--upload" "--verbose"); \ + fi; \ + echo """$(IMAGE_CMD) run --rm=true --env CODACY_CODE="/code" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --volume /tmp:/tmp --volume ".":"/code" \ + codacy/codacy-analysis-cli analyze "$${LARGS[@]}" --tool $*"""; \ + $(IMAGE_CMD) run --rm=true --env CODACY_CODE="/code" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --volume /tmp:/tmp --volume "$$(pwd):/code" \ + codacy/codacy-analysis-cli analyze "$${LARGS[@]}" --tool $* || \ + $(CODACY_CONTINUE); \ + +LINT_ARGS_GOSEC_LOCAL := -log /dev/null -exclude G105,G307 ./... +LINT_ARGS_GOSEC_UPLOAD := -log /dev/null -exclude G105,G307 -fmt json ./... +LINT_ARGS_STATICCHECK_LOCAL := -tests ./... +LINT_ARGS_STATICCHECK_UPLOAD := -tests -f json ./... + +$(TARGETS_LINT_CODACY_BINARIES): lint-%: init-sources init-% $(GOBIN)/% + @COMMIT=$$(git log --max-count=1 --pretty=format:"%H"); \ + VERSION="$(CODACY_$(call upper,$*)_VERSION)"; \ + LARGS=("--silent" "--location" "--request" "POST" \ + "--header" "Content-type: application/json"); \ + if [ -n "$(CODACY_PROJECT_TOKEN)" ]; then \ + BASE="$(CODACY_API_BASE_URL)/2.0/commit/$${COMMIT}"; \ + LARGS+=("-H" "project-token: $(CODACY_PROJECT_TOKEN)"); \ + elif [ -n "$(CODACY_API_TOKEN)" ]; then \ + SPATH="$(CODACY_PROVIDER)/$(CODACY_USER)/$(CODACY_PROJECT)"; \ + BASE="$(CODACY_API_BASE_URL)/2.0/$${SPATH}/commit/$${COMMIT}"; \ + LARGS+=("-H" "api-token: $(CODACY_API_TOKEN)"); \ + fi; \ + if [ -n "$${BASE}" ]; then \ + echo -e """$(GOBIN)/$* $(LINT_ARGS_$(call upper,$*)_UPLOAD) | \ + $(GOBIN)/codacy-$*-$${VERSION} | \ + curl "$${LARGS[@]}" --data @- "$${BASE}/issuesRemoteResults"; \ + curl "$${LARGS[@]}" "$${BASE}/resultsFinal""""; \ + ( $(GOBIN)/$* $(LINT_ARGS_$(call upper,$*)_UPLOAD) | \ + $(GOBIN)/codacy-$*-$${VERSION} | \ + curl "$${LARGS[@]}" --data @- "$${BASE}/issuesRemoteResults"; echo; \ + curl "$${LARGS[@]}" "$${BASE}/resultsFinal"; echo ) || \ + $(CODACY_CONTINUE); \ + else \ + echo $(GOBIN)/$* $(LINT_ARGS_$(call upper,$*)_LOCAL); \ + $(GOBIN)/$* $(LINT_ARGS_$(call upper,$*)_LOCAL) || $(CODACY_CONTINUE); \ + fi; \ + +#@ execute revive linter. +lint-revive: init-sources $(GOBIN)/revive + revive -formatter friendly -config=revive.toml $(SOURCES) || $(CODACY_CONTINUE); + +#@ execute markdown linter. +lint-markdown: init-sources $(NVM_BIN)/markdownlint + @echo markdownlint --config .markdownlint.yaml .; \ + if command -v markdownlint &> /dev/null; then \ + markdownlint --config .markdownlint.yaml .; \ + else $(IMAGE_CMD) run --tty --volume $$(pwd):/src:ro \ + container-registry.zalando.net/library/node-18-alpine:latest \ + /bin/sh -c "npm install --global markdownlint-cli >/dev/null 2>&1 && \ + cd /src && markdownlint --config .markdownlint.yaml ."; \ + fi; \ + +#@ execute shellcheck to find potential shell script issues. +lint-shell: init-sources + @mapfile -t FILES < <(find -name "*.sh"); ARGS="--color=always"; \ + if [ -n "$(RUNARGS)" ]; then ARGS=(${ARGS} $(RUNARGS)); fi; \ + if [ -n "$${FILES}" ]; then \ + if command -v shellcheck &> /dev/null; then \ + shellcheck "$${ARGS[@]}" "$${FILES[@]}"; \ + else \ + $(IMAGE_CMD) run --rm --volume="$${PWD}:/mnt" \ + koalaman/shellcheck:stable "$${ARGS[@]}" "$${FILES[@]}"; \ + fi; \ + fi; \ + +#@ execute gitleaks to check committed code for leaking secrets. +lint-leaks: $(GOBIN)/gitleaks + gitleaks detect --no-banner --verbose --source .; +#@ execute gitleaks to check un-committed code changes for leaking secrets. +lint-leaks?: $(GOBIN)/gitleaks + gitleaks protect --no-banner --verbose --source .; +#@ execute govulncheck to find vulnerabilities in dependencies. +lint-vuln: $(GOBIN)/govulncheck + govulncheck -test ./... +#@ # execute go(cyclo|cognit) linter. +lint-gocyclo lint-gocognit: lint-%: $(GOBIN)/% + @RUNARGS=($(RUNARGS)); MODS="0"; \ + while [ "$${#RUNARGS[@]}" != "0" ]; do \ + if [ "$${RUNARGS[0]}" == "top" ]; then ARGS+=(-$${RUNARGS[0]}); MODS="1"; \ + elif [ "$${RUNARGS[0]}" == "over" ]; then ARGS+=(-$${RUNARGS[0]}); MODS="1"; \ + elif [ "$${RUNARGS[0]}" == "avg" ]; then ARGS+=(-$${RUNARGS[0]}); MODS="0"; \ + else ARGS+=($${RUNARGS[0]}); MODS=$$((MODS-1)); fi; \ + RUNARGS=("$${RUNARGS[@]:1}"); \ + done; \ + if [[ "$${MODS}" == "0" ]]; then ARGS+=($(MODULES)); fi; \ + $* "$${ARGS[@]}"; + +#@ execute zally api-linter to find API issues. +lint-apis: $(GOBIN)/zally + @LINTER="https://infrastructure-api-linter.zalandoapis.com"; \ + if ! curl --silent $${LINTER}; then \ + echo "warning: API linter not available;"; exit 0; \ + fi; \ + ARGS=("--linter-service" "$${LINTER}"); \ + if command -v ztoken > /dev/null; then ARGS+=("--token" "$$(ztoken)"); fi; \ + if [ -n "$(RUNARGS)" ]; then FILES="$(RUNARGS)"; \ + else FILES="$$(find zalando-apis -name "*.yaml" 2>/dev/null)"; fi; \ + for APISPEC in $${FILES}; do \ + echo "check API: zally \"$${APISPEC}\""; \ + zally "$${ARGS[@]}" lint "$${APISPEC}" || exit 1; \ + done; + + +# Variables for setting up container specific build flags. +BUILD_OS ?= $(shell go env GOOS) +BUILD_ARCH ?= $(shell go env GOARCH) +IMAGE_OS ?= ${shell grep "^FROM [^ ]*$$" $(FILE_CONTAINER) 2>/dev/null | \ + grep -v " as " | sed "s/.*\(alpine\|ubuntu\).*/\1/g"} +ifeq ($(IMAGE_OS),alpine) + BUILD_FLAGS ?= -v -mod=readonly -buildvcs=auto + GOCGO := 0 +else + BUILD_FLAGS ?= -v -mod=readonly -buildvcs=auto -race + GOCGO := 1 +endif + +# Function to select right main package of command. +main-pkg = $(patsubst $(1)=%,%,$(firstword $(filter $(1)=%,$(COMMANDS_PKG)))) + +# Functions and variables to propagate versions to build commands. +LD-PATHS := main $(shell go list ./... | grep "config$$") +ld-flag = $(addprefix -X ,$(addsuffix .$(1),$(LD-PATHS))) +ld-flags = $(call ld-flag,Version=$(BUILD_VERSION)) \ + $(call ld-flag,Revision=$(shell git rev-parse HEAD)) \ + $(call ld-flag,Build=$(shell date --iso-8601=seconds)) \ + $(call ld-flag,Commit=$(shell git log -1 --date=iso8601-strict --format=%cd)) \ + $(call ld-flag,Dirty=$(shell git diff --quiet && echo false || echo true)) \ + $(call ld-flag,Path=$(REPOSITORY)$(patsubst .%,%,$(1))) \ + $(call ld-flag,GitHash=$(shell git rev-parse --short HEAD)) $(LDFLAGS) \ + + +## Build: targets to build native and linux platform executables. + +#@ build default executables (native). +build: build-native +#@ build native platform executables using system architecture. +build-native: $(TARGETS_BUILD) +$(TARGETS_BUILD): build-%: $(DIR_BUILD)/% +$(DIR_BUILD)/%: init-hooks $(SOURCES) + @mkdir -p "$(dir $@)"; + GOOS=$(BUILD_OS) GOARCH=$(BUILD_ARCH) CGO_ENABLED=1 \ + go build -ldflags="$(call ld-flags,$(call main-pkg,$*))" \ + $(BUILD_FLAGS) -o $@ $(call main-pkg,$*)/main.go; + +#@ build linux platform executables using default (system) architecture. +build-linux: $(TARGETS_BUILD_LINUX) +$(DIR_BUILD)/linux/%: $(call main-pkg,%) init-hooks $(SOURCES) + @mkdir -p "$(dir $@)"; + GOOS=$(BUILD_OS) GOARCH=$(BUILD_ARCH) CGO_ENABLED=$(GOCGO) \ + go build -ldflags="$(call ld-flags,$(call main-pkg,$*))" \ + $(BUILD_FLAGS) -o $@ $(call main-pkg,$*)/main.go; + +#@ build container image (alias for image-build). +build-image: image-build + + +## Image: targets to build and push container images. + +#@ build and push container images - if setup. +image: $(if $(filter $(IMAGE_PUSH),never),,image-push) +#@ build container images. +image-build: $(TARGETS_IMAGE_BUILD) + +setup-image-file = \ + FILE="$*"; IMAGE=$(IMAGE); \ + PREFIX="$$(basename "$${FILE%$(FILE_CONTAINER)*}")"; \ + SUFFIX="$$(echo $${FILE\#*$(FILE_CONTAINER)} | sed "s/^[^[:alnum:]]*//")"; \ + INFIX="$${SUFFIX:-$${PREFIX}}"; \ + if [ -n "$${INFIX}" ]; then \ + IMAGE="$${IMAGE/:/-$${INFIX}:}"; \ + fi; + +$(TARGETS_IMAGE_BUILD): image-build/%: build-linux + @$(call setup-image-file $*) \ + if [ "$(IMAGE_PUSH)" == "never" ]; then \ + echo "We never build images, aborting [$${IMAGE}]."; exit 0; \ + fi; \ + REVISION=$$(git rev-parse HEAD 2>/dev/null); \ + BASE=$$(grep "^FROM[[:space:]]" $${FILE} | tail -n1 | cut -d" " -f2); \ + $(IMAGE_CMD) build --tag $${IMAGE} --file=$${FILE} . \ + --label=org.opencontainers.image.created="$$(date --rfc-3339=seconds)" \ + --label=org.opencontainers.image.vendor=zalando/$(TEAM) \ + --label=org.opencontainers.image.source=$(REPOSITORY) \ + --label=org.opencontainers.image.version=$${IMAGE##*:} \ + --label=org.opencontainers.image.revision=$${REVISION} \ + --label=org.opencontainers.image.base.name=$${BASE} \ + --label=org.opencontainers.image.ref.name=$${IMAGE}; \ + +#@ push contianer images - if setup and allowed. +image-push: $(TARGETS_IMAGE_PUSH) +$(TARGETS_IMAGE_PUSH): image-push/%: image-build/% + @$(call setup-image-file $*) \ + if [ "$(IMAGE_PUSH)" == "never" ]; then \ + echo "We never push images, aborting [$${IMAGE}]."; exit 0; \ + elif [ "$(IMAGE_VERSION)" == "snapshot" ]; then \ + echo "We never push snapshot images, aborting [$${IMAGE}]."; exit 0; \ + elif [ -n "$(CDP_PULL_REQUEST_NUMBER)" -a "$(IMAGE_PUSH)" != "pulls" ]; then \ + echo "We never push pull request images, aborting [$${IMAGE}]."; exit 0; \ + fi; \ + $(IMAGE_CMD) push $${IMAGE}; \ + + +## Run: targets for starting services and commands for testing. +HOST := "127.0.0.1" + +#@ starts a postgres database instance for testing. +run-db: $(DIR_RUN) + @if [[ ! " $(TEST_DEPS) $(RUN_DEPS) " =~ " run-db " ]]; then exit 0; fi; \ + echo "info: ensure $(DB_IMAGE) running on $(HOST):$(DB_PORT)"; \ + if [ -n "$$($(IMAGE_CMD) ps | grep "$(DB_IMAGE).*$(HOST):$(DB_PORT)")" ]; then \ + echo "info: port allocated, use existing db container!"; exit 0; \ + fi; \ + $(IMAGE_CMD) start ${IMAGE_ARTIFACT}-db 2>/dev/null || ( \ + $(IMAGE_CMD) run --detach --tty \ + --name ${IMAGE_ARTIFACT}-db \ + --publish $(HOST):$(DB_PORT):5432 \ + --env POSTGRES_USER="$(DB_USER)" \ + --env POSTGRES_PASSWORD="$(DB_PASSWORD)" \ + --env POSTGRES_DB="$(DB_NAME)" $(DB_IMAGE) \ + -c 'shared_preload_libraries=pg_stat_statements' \ + -c 'pg_stat_statements.max=10000' \ + -c 'pg_stat_statements.track=all' \ + $(RUNARGS) 2>&1 & \ + until [ "$$($(IMAGE_CMD) inspect --format {{.State.Running}} \ + $(IMAGE_ARTIFACT)-db 2>/dev/null)" == "true" ]; \ + do echo "waiting for db container" >/dev/stderr; sleep 1; done && \ + until $(IMAGE_CMD) exec $(IMAGE_ARTIFACT)-db \ + pg_isready -h localhost -U $(DB_USER) -d $(DB_NAME); \ + do echo "waiting for db service" >/dev/stderr; sleep 1; done) |\ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-db; \ + +#@ starts an AWS localstack instance for testing. +run-aws: $(DIR_RUN) + @if [[ ! " $(TEST_DEPS) $(RUN_DEPS) " =~ " run-aws " ]]; then exit 0; fi; \ + echo "info: ensure $(AWS_IMAGE) is running on $(HOST):4566/4571" && \ + if [ -n "$$($(IMAGE_CMD) ps | \ + grep "$(AWS_IMAGE).*$(HOST):4566.*$(HOST):4571")" ]; then \ + echo "info: ports allocated, use existing aws container!"; \ + $(call run-aws-setup); exit 0; \ + fi; \ + $(IMAGE_CMD) start ${IMAGE_ARTIFACT}-aws 2>/dev/null || ( \ + $(IMAGE_CMD) run --detach --tty --name ${IMAGE_ARTIFACT}-aws \ + --publish $(HOST):4566:4566 --publish $(HOST):4571:4571 \ + --env SERVICES="$(AWS_SERVICES)" $(AWS_IMAGE) $(RUNARGS) 2>&1 && \ + until [ "$$($(IMAGE_CMD) inspect --format {{.State.Running}} \ + $(IMAGE_ARTIFACT)-aws 2>/dev/null)" == "true" ]; \ + do echo "waiting for aws container" >/dev/stderr; sleep 1; done && \ + until $(IMAGE_CMD) exec $(IMAGE_ARTIFACT)-aws \ + curl --silent http://$(HOST):4566; \ + do echo "waiting for aws service" >/dev/stderr; sleep 1; done && \ + $(call run-aws-setup)) | \ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-aws.log; \ + +#@ run-*: starts the provide command using the native binary. +$(TARGETS_RUN): run-%: $(DIR_BUILD)/% $(RUN_DEPS) $(DIR_RUN) $(DIR_CRED) + @$(call run-setup); $(call run-vars) $(call run-vars-local) \ + $(DIR_BUILD)/$* $(RUNARGS) 2>&1 | \ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-$*.log; \ + exit $${PIPESTATUS[0]}; + +#@ run-go-*: starts the provide command using go run. +$(TARGETS_RUN_GO): run-go-%: $(DIR_BUILD)/% $(RUN_DEPS) $(DIR_RUN) $(DIR_CRED) + @$(call run-setup); $(call run-vars) $(call run-vars-local) \ + go run cmd/$*/main.go $(RUNARGS) 2>&1 | \ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-$*.log; \ + exit $${PIPESTATUS[0]}; + +#@ run-image-*: starts the matched command via the container images. +$(TARGETS_RUN_IMAGE): run-image-%: $(RUN_DEPS) $(DIR_RUN) $(DIR_CRED) + @trap "$(IMAGE_CMD) rm $(IMAGE_ARTIFACT)-$* >/dev/null" EXIT; \ + trap "$(IMAGE_CMD) kill $(IMAGE_ARTIFACT)-$* >/dev/null" INT TERM; \ + if [ -n "$(filter %$*,$(TARGETS_IMAGE))" ]; then \ + IMAGE="$$(echo "$(IMAGE)" | sed -e "s/:/-$*:/")"; \ + else IMAGE="$(IMAGE)" fi; $(call run-setup); \ + $(IMAGE_CMD) run --name $(IMAGE_ARTIFACT)-$* --network=host \ + --volume $(DIR_CRED):/meta/credentials --volume $(DIR_RUN)/temp:/tmp \ + $(call run-vars, --env) $(call run-vars-image, --env) \ + ${IMAGE} /$* $(RUNARGS) 2>&1 | \ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-$*.log; \ + exit $${PIPESTATUS[0]}; + +#@ clean up all running container images. +run-clean: $(TARGETS_RUN_CLEAN) +#@ run-clean-*: kills and removes the container image of the matched command. +$(TARGETS_RUN_CLEAN): run-clean-%: + @echo "check container $(IMAGE_ARTIFACT)-$*"; \ + if [ -n "$$($(IMAGE_CMD) ps | grep "$(IMAGE_ARTIFACT)-$*")" ]; then \ + $(IMAGE_CMD) kill $(IMAGE_ARTIFACT)-$* > /dev/null && \ + echo "killed container $(IMAGE_ARTIFACT)-$*"; \ + fi; \ + if [ -n "$$($(IMAGE_CMD) ps -a | grep "$(IMAGE_ARTIFACT)-$*")" ]; then \ + $(IMAGE_CMD) rm $(IMAGE_ARTIFACT)-$* > /dev/null && \ + echo "removed container $(IMAGE_ARTIFACT)-$*"; \ + fi; \ + + +## Cleanup: targets to clean up sources, tools, and containers. + +#@ clean up resources created during build processes. +clean: $(TARGETS_CLEAN) +#@ clean up all resources, i.e. also tools installed for the build. +clean-all: $(TARGETS_CLEAN_ALL) +#@ clean up all resources created by initialization. +clean-init:; rm -vrf .git/hooks/pre-commit; +#@ clean up all resources created by building and testing. +clean-build:; rm -vrf $(DIR_BUILD) $(DIR_RUN); + $(FIND) -name "mock_*_test.go" -exec rm -v {} \;; +#@ clean up all package dependencies by removing packages. +clean-deps: + for PACKAGE in $$(cat go.sum | cut -d ' ' -f 1 | sort -u); do \ + go get $${PACKAGE}@none; \ + done; +#@ clean up all running container images. +clean-run: $(TARGETS_CLEAN_RUN) +#@ clean-run-*: clean up matched running container image. +$(TARGETS_CLEAN_RUN): clean-run-%: run-clean-% + + +## Update: targets to update tools, configs, and dependencies. + +# TODO: +# 'update-deps' check how to utilize: 'go list -m -u -mod=mod all' + + +#@ update default or customized update targets. +update: $(TARGETS_UPDATE) +#@ check default or customized update targets for updates. +update?: $(TARGETS_UPDATE?) +#@ update all components. +update-all: $(TARGETS_UPDATE_ALL) +#@ check all components for udpdates. +update-all?: $(TARGETS_UPDATE_ALL?) + +#@ # update go to latest or given version. +update-go: + @if ! [[ "$(RUNARGS)" =~ "[0-9.]*" ]]; then \ + ARCH="linux-amd64"; BASE="https://go.dev/dl"; \ + VERSION="$$(curl --silent --header "Accept-Encoding: gzip" \ + "$${BASE}/" | gunzip | grep -o "go[0-9.]*$${ARCH}.tar.gz" | \ + head -n 1 | sed "s/go\(.*\)\.[0-9]*\.$${ARCH}\.tar\.gz/\1/")"; \ + else VERSION="$(RUNARGS)"; fi; \ + echo "info: update golang version to $${VERSION}"; \ + go mod tidy -go=$${VERSION}; \ + if [ -f $(FILE_DELIVERY) ]; then \ + sed -E -i -e "s/$(FILE_DELIVERY_REGEX)/\1$${VERSION}/" \ + $(FILE_DELIVERY); \ + fi; \ + if [ "$(GOVERSION)" != "$${VERSION}" ]; then \ + echo "warning: current compiler is using $(GOVERSION)" >/dev/stderr; \ + fi; \ + +#@ check whether a new go version exists. +update-go?: + @ARCH="linux-amd64"; BASE="https://go.dev/dl"; \ + VERSION="$$(curl --silent --header "Accept-Encoding: gzip" \ + "$${BASE}/" | gunzip | grep -o "go[0-9.]*$${ARCH}.tar.gz" | \ + head -n 1 | sed "s/go\(.*\)\.[0-9]*\.$${ARCH}\.tar\.gz/\1/")"; \ + if [ "$(GOVERSION)" != "$${VERSION}" ]; then \ + echo "info: new golang version $${VERSION} available"; \ + echo -e "\trun 'make update-go' to update the project!"; \ + exit -1; \ + fi; \ + +# Functions to create the list of updates (not sure whether go list is contributing). +update-args = \ + if [[ " $(1) " =~ " major " ]]; then ARGS="-cached false -major"; \ + elif [[ " $(1) " =~ " pre " ]]; then ARGS="-cached false -pre"; \ + else ARGS="-cached false"; fi +update-list = \ + ( gomajor list $(1) 2>/dev/null | sed "s/://1; s/\[latest /\[/g"; \ + go list -m -u -mod=mod $(2) | grep "\[.*\]$$" \ + ) | sort -u | sed "s/\]//g; s/\[/=> /" + +#@ [major|pre|minor] # updates minor (and major) dependencies to latest versions. +update-deps: test-go update/go.mod +update/go.mod: $(GOBIN)/gomajor + @if [ -n "$(RUNARGS)" ]; then $(call update-args,$(RUNARGS)); \ + if [[ " $(RUNARGS) " =~ " minor " ]]; then PACKAGES="all"; fi; \ + readarray -t UPDATES < <($(call update-list,$${ARGS},$${PACKAGES})); \ + for UPDATE in "$${UPDATES[@]}"; do ARGS=($${UPDATE}); \ + SMAJOR="$${ARGS[1]%%.*}"; TMAJOR="$${ARGS[3]%%.*}"; \ + if [ "$${SMAJOR}" != "$${TMAJOR}" ]; then \ + if [[ "$${ARGS[0]}" == *$${SMAJOR}* ]]; then \ + TARGET="$${ARGS[0]/$${SMAJOR}/$${TMAJOR}}"; \ + else TARGET="$${ARGS[0]}/$${TMAJOR}"; fi; \ + echo "update: $${ARGS[0]}@$${ARGS[2]} => $${TARGET}@$${ARGS[3]}"; \ + sed -i -e "s#$${ARGS[0]}#$${TARGET}#g" $(SOURCES); \ + else \ + echo "update: $${ARGS[0]}@$${ARGS[1]} => $${ARGS[3]}"; \ + go get "$${ARGS[0]}@$${ARGS[3]}"; \ + fi; \ + done; \ + else go get -u ./...; fi; \ + ROOT=$$(pwd); \ + for DIR in $(MODULES); do \ + echo "update: $${DIR}"; cd $${ROOT}/$${DIR##./} && \ + go mod tidy -x -v -e -compat=$(GOVERSION) || exit -1; \ + done; \ + +#@ checks for major and minor updates to latest versions. +update-deps?: test-go update/go.mod? +update/go.mod?: $(GOBIN)/gomajor + @$(call update-args,$(RUNARGS)); cp go.sum go.sum.~save~; \ + $(call update-list,$${ARGS},all); mv go.sum.~save~ go.sum; + +#@ update this build environment to latest version. +update-make: $(TARGETS_UPDATE_MAKE) +$(TARGETS_UPDATE_MAKE): update/%: update-base + @DIR="$$(pwd)"; cd $(BASEDIR); \ + if [ ! -e "$${DIR}/$*" ]; then touch "$${DIR}/$*"; fi; \ + DIFF="$$(diff <(git show HEAD:$* 2>/dev/null) $${DIR}/$*)"; \ + if [ -n "$${DIFF}" ]; then \ + if [ -n "$$(cd $${DIR}; git diff $*)" ]; then \ + echo "info: $* is blocked (has been changed)"; \ + else echo "info: $* is updated"; \ + git show HEAD:$* > $${DIR}/$* 2>/dev/null; \ + fi; \ + fi; \ + +#@ check whether updates for build environment exist. +update-make?: $(TARGETS_UPDATE_MAKE?) +$(TARGETS_UPDATE_MAKE?): update/%?: update-base + @DIR="$$(pwd)"; cd $(BASEDIR); \ + if [ ! -e "$${DIR}/$*" ]; then \ + echo "info: $* is not tracked"; \ + else DIFF="$$(diff <(git show HEAD:$*) $${DIR}/$*)"; \ + if [ -n "$${DIFF}" ]; then \ + if [ -n "$$(cd $${DIR}; git diff $*)" ]; then \ + echo "info: $* is blocked (has been changed)"; \ + else echo "info: $* needs update"; fi; \ + fi; \ + fi; \ + +#@ creates a clone of the base repository to update from. +BASE := git@github.com:tkrop/go-make.git +update-base: + @$(eval BASEDIR = $(shell mktemp -d)) ( \ + trap "rm -rf $${DIR}" INT TERM EXIT; \ + while pgrep $(MAKE) > /dev/null; do sleep 1; done; \ + rm -rf $${DIR} \ + ) & \ + git clone --no-checkout --depth 1 $(BASE) $(BASEDIR) 2> /dev/null; \ + +#@ updates all tools required by the project. +update-tools: $(TARGETS_UPDATE_GO) $(TARGETS_INSTALL_SH) $(TARGETS_INSTALL_NPM) + go mod tidy -compat=${GOVERSION}; +#@ update-*: updates the matched software command or service. + +# update go tools used by the project. +$(TARGETS_UPDATE_GO): update-%: + go install $(call go-pkg,update,$(TOOLS_GO),^$*$$); + +## Custom: custom extension targets. + +# Include custom targets to extend scripts. +ifneq ("$(wildcard Makefile.ext)","") + include Makefile.ext +endif diff --git a/Makefile.ext b/Makefile.ext new file mode 100644 index 0000000..d04195e --- /dev/null +++ b/Makefile.ext @@ -0,0 +1,8 @@ +.PHONY: install-go-make uninstall-go-make +install-go-make: uninstall-go-make + mkdir -p $(GOBIN)/go-make.config; + cp -rf . $(GOBIN)/go-make.config; + +uninstall-go-make: uninstall-go-make.config +uninstall-go-make.config: + rm -rf $(GOBIN)/go-make.config; diff --git a/Makefile.vars b/Makefile.vars new file mode 100644 index 0000000..b80ad79 --- /dev/null +++ b/Makefile.vars @@ -0,0 +1,30 @@ +# Setup code quality level (default: base). +CODE_QUALITY := plus + +# Setup codacy integration (default: enabled [enabled, disabled]). +CODACY := enabled +# Customizing codacy server for open source. +CODACY_API_BASE_URL := https://api.codacy.com +# (default: false / true [cdp-pipeline]) +#CODACY_CONTINUE := true + +# Setup required targets before testing (default: ). +#TEST_DEPS := run-db +# Setup required targets before running commands (default: ). +#RUN_DEPS := run-db +# Setup required aws services for testing (default: ). +#AWS_SERVICES := + +# Setup when to push images (default: pulls [never, pulls, merges]) +IMAGE_PUSH ?= never + +# Setup default test timeout (default: 10s). +TEST_TIMEOUT := 12s + +# Setup custom delivery file (default: delivery.yaml). +FILE_DELIVERY := .github/workflows/go.yaml + +# Custom linters applied to prepare next level (default: ). +LINTERS_CUSTOM := nonamedreturns tagliatelle +# Linters swithed off to complete next level (default: ). +LINTERS_DISABLED := diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7a1153 --- /dev/null +++ b/README.md @@ -0,0 +1,226 @@ +# Make framework + +[![Build][build-badge]][build-link] +[![Coverage][coveralls-badge]][coveralls-link] +[![Coverage][coverage-badge]][coverage-link] +[![Quality][quality-badge]][quality-link] +[![Report][report-badge]][report-link] +[![FOSSA][fossa-badge]][fossa-link] +[![License][license-badge]][license-link] +[![Docs][docs-badge]][docs-link] + + +[build-badge]: https://github.com/tkrop/go-make/actions/workflows/go.yaml/badge.svg +[build-link]: https://github.com/tkrop/go-make/actions/workflows/go.yaml + +[coveralls-badge]: https://coveralls.io/repos/github/tkrop/go-make/badge.svg?branch=main +[coveralls-link]: https://coveralls.io/github/tkrop/go-make?branch=main + +[coverage-badge]: https://app.codacy.com/project/badge/Coverage/cc1c47ec5ce0493caf15c08fa72fc78c +[coverage-link]: https://www.codacy.com/gh/tkrop/go-make/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tkrop/go-make&utm_campaign=Badge_Coverage + +[quality-badge]: https://app.codacy.com/project/badge/Grade/cc1c47ec5ce0493caf15c08fa72fc78c +[quality-link]: https://www.codacy.com/gh/tkrop/go-make/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tkrop/go-make&utm_campaign=Badge_Grade + +[report-badge]: https://goreportcard.com/badge/github.com/tkrop/go-make +[report-link]: https://goreportcard.com/report/github.com/tkrop/go-make + +[fossa-badge]: https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftkrop%2Ftesting.svg?type=shield +[fossa-link]: https://app.fossa.com/projects/git%2Bgithub.com%2Ftkrop%2Ftesting?ref=badge_shield + +[license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg +[license-link]: https://opensource.org/licenses/MIT + +[docs-badge]: https://pkg.go.dev/badge/github.com/tkrop/go-make.svg +[docs-link]: https://pkg.go.dev/github.com/tkrop/go-make + + + +## Introduction + +Goal of `go-make` is to provide a simple, versioned build environment for +standard [`go`][go]-projects (see [Standard `go`-project](#standard-go-project) +for details) providing default targets and tool configs for testing, linting, +building, installing, updating, running, and releasing libraries, commands, and +container images. + +`go-make` can be either run as command line tool or hooked into an existing +project as minimal [`Makefile`](Makefile.temp). Technically `go-make` is just a +thin wrapper around a very generic and extensible [`Makefile`](Makefile.base) +that is based on a standard [`go`][go]-project supporting different tools: + +* [`gomock`][gomock] - go generating mocks. +* [`codacy`][codacy] - for code quality documentation. +* [`golangci-lint`][golangci] - for pre-commit linting. +* [`zally`][zally] - for pre-commit API linting. +* [`gitleaks`][gitleaks] - for sensitive data scanning. +* [`grype`][grype] - for security scanning. +* [`syft`][syft] - for material listing. + +The thin wrapper provides the necessary version control for the `Makefile` and +the default config of integrated tools. It installs these tools automatically +when needed in the latest available version. + +**Note:** We except the risk that using the latest versions of tools, e.g. for +linting, may break the build for the sake of constantly updating dependencies by +default. For tools where this is not desireable, the default import can be +changed to contain a version (see [manual](MANUAL.md] for more information). + +**Warning:** `go-make` automatically installs a `pre-commit` hook overwriting +and deleting any pre-existing hook. The hook calls `go-make commit` to enforce +run unit testing and linting successfully before allowing to commit, i.e. the +goals `test-go`, `test-unit`, `lint-base` (or what code quality level is +defined as standard), and `lint-markdown`. + + +[go]: +[gomock]: +[golangci]: +[codacy]: +[zally]: +[gitleaks]: +[grype]: +[syft]: + + +## Installation + +To install `go-make` simply use the standard [`go` install][go-install] +command (or any other means, e.g. [`curl`][curl] to obtain a released binary): + +```bash +go install github.com/tkrop/go-make@latest +``` + +The scripts and configs are automatically checked out in the version matching +the wrapper. `go-make` has the following dependencies, that must be satisfied +by the runtime environment, e.g. using [`ubuntu-20.04`][ubuntu-20.04] or +[`ubuntu-22.04`][ubuntu-22.04]: + +* [GNU `make`][make] (^4.2). +* [GNU `bash`][bash] (^5.0). +* [GNU `coreutils`][core] (^8.30) +* [GNU `findutils`][find] (^4.7) +* [GNU `awk`][awk] (^5.0). +* [GNU `sed`][sed] (^4.7) +* [`curl`][curl] (^7) + +[ubuntu-20.04]: +[ubuntu-22.04]: +[go-install]: +[curl]: +[make]: +[bash]: +[core]: +[find]: +[awk]: +[sed]: + + +## Example usage + +After installing `go-make` and in the build environment, you can run all targets +by simply calling `go-make ` on the command line, in another `Makefile`, +in a github action, or any other delivery pipeline config script: + +```bash +go-make all # execute a whole build pipeline depending on the project. +go-make test lint # execute only test 'test' and 'lint' steps of a pipeline. +go-make image # execute minimal steps to create all container images. +``` + +If you like to integrate `go-make` into another `Makefile` you may find the +following template helpful: + +```Makefile +GOBIN ?= $(shell go env GOPATH)/bin +GOMAKE := github.com/tkrop/go-make@latest +TARGETS := $(shell command -v go-make >/dev/null || \ + go install $(GOMAKE) && go-make targets) + +# Include standard targets from go-make providing group targets as well as +# single target targets. The group target is used to delegate the remaining +# request targets, while the single target can be used to define the +# precondition of custom target. +.PHONY: $(TARGETS) $(addprefix target/,$(TARGETS)) +$(TARGETS):; $(GOBIN)/go-make $(MAKEFLAGS) $(MAKECMDGOALS); +$(addprefix target/,$(TARGETS)): target/%: + $(GOBIN)/go-make $(MAKEFLAGS) $*; +``` + +For further examples see [`go-make` manual](MANUAL.md). + + +## Standard `go`-Project + +A standard [`go`][go]-project is defined to meet Zalando in-house requirements, +but is general enough to be useful in open source projects too. It adheres to +the following conventions: + +1. All commands (services, jobs) are provided by a `main.go` placed as usual + under `cmd` using the pattern `cmd//main.go` or in the root folder. In + the later case the project name is used as command name. + +2. All source code files and package names following the [`go`][go]-standard + only consist of lower case ascii letters, hyphens (`-`), underscores (`_`), + and dots (`.`). Files are ending with `.go` to be eligible. + +3. Modules are placed in any sub-path of the repository, e.g. in `pkg`, `app`, + `internal` are commonly used patterns, except for `build` and `run`. These + are used by `go-make` as temporary folders to build commands and run commands + and are cleaned up regularly. + +4. The build target provides build context values to setup global variables in + `main` and `config` packages. + + * `Path` - the formal package path of the command build. + * `Version` - the version as provided by the `VERSION`-file in project root + or via the `(BUILD_)VERSION` environ variables. + * `Revision` - the actual full commit hash (`git rev-parse HEAD`). + * `Build` - the current timestamp of the build (`date --iso-8601=seconds`). + * `Commit` - the timestamp of the actual commit timestamp + (`git log -1 --date=iso8601-strict --format=%cd`). + * `Dirty` - the information whether build repository had uncommitted changes. + +5. All container image build files must start with a common prefix (default is + `Dockerfile`). The image name is derived from the organization and repository + names and can contain an optional suffix, i.e `/(-)`. + +6. For running a command in a container image, make sure that the command is + installed in the default execution directory of the container image - usually + the root directory. The container image must either be generated with suffix + matching the command or without suffix. + +All targets in the `go-make` are designated to work *out-of-the-box* taking self +care to setup the [`go`][go]-project, installing the necessary tools, except for +the golang compiler and build environment (see [installation](#installation)), +and triggering the precondition targets as necessary. + + +## Terms of usage + +This software is open source as is under the MIT license. If you start using +the software, please give it a star, so that I know to be more careful with +changes. If this project has more than 25 Stars, I will introduce semantic +versions for changes. + + +## Building + +The project is using itself for building as a proof of concept. + + +## Contributing + +If you like to contribute, please create an issue and/or pull request with a +proper description of your proposal or contribution. I will review it and +provide feedback on it. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3ce36f3 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/tkrop/go-make + +go 1.21 + +toolchain go1.21.0 + +require golang.org/x/exp v0.0.0-20230905200255-921286631fa9 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa6da3f --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3f9802e --- /dev/null +++ b/main.go @@ -0,0 +1,303 @@ +package main + +import ( + _ "embed" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "runtime/debug" + "strconv" + "strings" + "time" + + "golang.org/x/exp/slices" +) + +var ( + // Path contains the package path. + Path string + // Version contains the custom version. + Version string + // Build contains the custom build time. + Build string + // Revision contains the custom revision. + Revision string + // Commit contains the custom commit time. + Commit string + // Dirty contains the custom dirty flag. + Dirty string +) + +var ( + // RepoPathSepNum is the number of the repository path separator. + RepoPathSepNum = 3 + // GitSha1HashLen is the full length of sha1-hashes used in git. + GitFullHashLen = 40 + // GitShortHashLen is the short length of sha1-hashes used in git. + GitShortHashLen = 7 +) + +var ( + info = NewInfo(Path, Version, Revision, Build, Commit, Dirty) + prog, _ = os.Executable() + config = prog + ".config" + makefile = filepath.Join(config, "Makefile.base") + wd, _ = os.Getwd() + trace = slices.Contains(os.Args, "--trace") +) + +const bashCompletion = `### bash completion for go-make +function __complete_go-make() { + COMPREPLY=($(compgen -W "$(go-make targets)" -- "${COMP_WORDS[COMP_CWORD]}")); +} +complete -F __complete_go-make go-make; +` + +func main() { + switch { + case trace: + LogInfo(os.Stderr, false) + LogCall(os.Stderr) + + case slices.Contains(os.Args, "--version"): + LogInfo(os.Stderr, true) + os.Exit(0) + + case slices.Contains(os.Args, "--completion"): + fmt.Fprint(os.Stdout, bashCompletion) + os.Exit(0) + } + + if err := UpdateGoMake(); err != nil { + LogError(os.Stderr, "update config", err) + os.Exit(1) + } + if err := Make(); err != nil { + if trace { + LogError(os.Stderr, "execute make", err) + } + os.Exit(1) + } +} + +func LogInfo(writer io.Writer, raw bool) { + if out, err := json.Marshal(info); err != nil { + fmt.Fprintf(writer, "info: %v\n", err) + } else if raw { + fmt.Fprintf(writer, "info: %s\n", out) + } else { + fmt.Fprintf(writer, "%s\n", out) + } +} + +func LogCall(writer io.Writer) { + fmt.Fprintf(writer, "call: %s\n", strings.Join(os.Args, " ")) +} + +func LogError(writer io.Writer, message string, err error) { + LogInfo(os.Stderr, false) + LogCall(os.Stderr) + if err != nil { + fmt.Fprintf(writer, "error: %s: %v\n", message, err) + } else { + fmt.Fprintf(writer, "error: %s\n", message) + } +} + +func UpdateGoMake() error { + if _, err := os.Stat(config); os.IsNotExist(err) { + if err := CloneRepo(); err != nil { + return err + } else if err := SetRevision(); err != nil { + return err + } + return nil + } else if err != nil { + return fmt.Errorf("config failure [%s]: %w", config, err) + } + + if revision, err := GetRevision(); err != nil { + return err + } else if !strings.HasPrefix(revision, info.Revision) { + if err := UpdateRevision(); err != nil { + return err + } + } + + return nil +} + +func CloneRepo() error { + if err := Call(os.Stderr, os.Stderr, wd, + "git", "clone", "--depth=1", info.Repo, config); err != nil { + repo := "https://" + info.Path + ".git" + return Call(os.Stderr, os.Stderr, wd, + "git", "clone", "--depth=1", repo, config) + } + return nil +} + +func UpdateRepo() error { + return Call(os.Stderr, os.Stderr, config, + "git", "fetch", info.Repo) +} + +func UpdateRevision() error { + if err := UpdateRepo(); err != nil { + return err + } + return SetRevision() +} + +func GetRevision() (string, error) { + builder := strings.Builder{} + if err := Call(&builder, os.Stderr, config, + "git", "rev-parse", "HEAD"); err != nil { + return "", err + } + return builder.String()[0:GitFullHashLen], nil +} + +func SetRevision() error { + revision := info.Revision + if len(revision) < GitFullHashLen { + revision = revision[0:GitShortHashLen] + } + return Call(os.Stderr, os.Stderr, config, + "git", "reset", "--hard", revision) +} + +func Make() error { + args := append([]string{"--file", makefile}, os.Args[1:]...) + return Call(os.Stdout, os.Stderr, wd, "make", args...) +} + +func Call( + stdout, stderr io.Writer, dir, name string, args ...string, +) error { + if trace { + fmt.Fprintf(os.Stdout, "%s %s [%s]\n", + name, strings.Join(args, " "), dir) + } + + cmd := exec.Command(name, args...) + cmd.Dir, cmd.Env = dir, os.Environ() + cmd.Env = append(cmd.Env, "MAKE=go-make") + cmd.Stdout, cmd.Stderr = stdout, stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("call failed [name=%s, args=%v]: %w", + name, args, err) + } + return nil +} + +// Info provides the build information of a command or module. +type Info struct { + // Path contains the package path of the command or module. + Path string `yaml:"path,omitempty" json:"path,omitempty"` + // Repo contains the repository of the command or module. + Repo string `yaml:"repo,omitempty" json:"repo,omitempty"` + // Version contains the actual version of the command or module. + Version string `yaml:"version,omitempty" json:"version,omitempty"` + // Revision contains the revision of the command or module from version + // control system. + Revision string `yaml:"revision,omitempty" json:"revision,omitempty"` + // Build contains the build time of the command or module. + Build time.Time `yaml:"build,omitempty" json:"build,omitempty"` + // Commit contains the last commit time of the command or module from the + // version control system. + Commit time.Time `yaml:"commit,omitempty" json:"commit,omitempty"` + // Dirty flags whether the build of the command or module is based on a + // dirty local repository state. + Dirty bool `yaml:"dirty,omitempty" json:"dirty,omitempty"` + // Checksum contains the check sum of the command or module. + Checksum string `yaml:"checksum,omitempty" json:"checksum,omitempty"` + + // Go contains the go version the command or module was build with. + Go string `yaml:"go,omitempty" json:"go,omitempty"` + // Platform contains the build platform the command or module was build + // for. + Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` + // Compiler contains the actual compiler the command or module was build + // with. + Compiler string `yaml:"compiler,omitempty" json:"compiler,omitempty"` +} + +var semVersionTag = regexp.MustCompile( + `v(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)` + + `(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)` + + `(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` + + `(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) + +// NewInfo returns the build information of a command or module using given +// custom version and custom build time using RFC3339 format. The provided +// version must follow semantic versioning as supported by go. +func NewInfo(path, version, revision, build, commit, dirty string) *Info { + i := &Info{ + Go: runtime.Version()[2:], + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + } + + i.Version = version + i.Revision = revision + i.Build, _ = time.Parse(time.RFC3339, build) + i.Commit, _ = time.Parse(time.RFC3339, commit) + i.Dirty, _ = strconv.ParseBool(dirty) + if info, ok := debug.ReadBuildInfo(); ok { + if path != "" { + i.Path = path + i.Repo = "git@" + strings.Replace( + SplitRuneN(path, '/', RepoPathSepNum), "/", ":", 1) + } else { + i.Path = info.Main.Path + i.Repo = "git@" + strings.Replace(info.Main.Path, "/", ":", 1) + } + + if semVersionTag.MatchString(info.Main.Version) { + i.Version = info.Main.Version + index := strings.LastIndex(info.Main.Version, "-") + i.Revision = info.Main.Version[index+1:] + } + + i.Checksum = info.Main.Sum + for _, kv := range info.Settings { + switch kv.Key { + case "vcs.revision": + i.Revision = kv.Value + case "vcs.time": + i.Commit, _ = time.Parse(time.RFC3339, kv.Value) + case "vcs.modified": + i.Dirty = kv.Value == "true" + } + } + } + + if !semVersionTag.MatchString(i.Version) { + if i.Revision != "" && !i.Commit.Equal(time.Time{}) { + i.Version = fmt.Sprintf("v0.0.0-%s-%s", + i.Commit.UTC().Format("20060102150405"), i.Revision[0:12]) + } + } + + return i +} + +func SplitRuneN(s string, ch rune, n int) string { + count := 0 + index := strings.IndexFunc(s, func(is rune) bool { + if ch == is { + count++ + } + return count == n + }) + if index >= 0 { + return s[0:index] + } + return s +} diff --git a/revive.toml b/revive.toml new file mode 100644 index 0000000..4cb6f64 --- /dev/null +++ b/revive.toml @@ -0,0 +1,95 @@ +# Translates the 'golangci-lint' settings into an codacy understandable format. + +# When set to false, ignores files with "GENERATED" header. +ignoreGeneratedHeader = true + +# Sets the default severity to "warning" +severity = "error" + +# Sets the default failure confidence. This means that linting errors +# with less than 0.8 confidence will be ignored. +confidence = 0.8 + +# Sets the error code for failures with severity "error" +errorCode = 1 + +# Sets the error code for failures with severity "warning" +warningCode = 0 + +# Enables all available rules +enableAllRules = true + +# No need to enforce a file header. +[rule.file-header] + disabled = true + +# Reports on each file in a package. +[rule.package-comments] + disabled = true + +# Reports on comments mismatching comments. +[rule.exported] + disabled = true + +# No need to exclude import shadowing. +[rule.import-shadowing] + disabled = true + +# Fails to exclude nolint directives from reporting. +# Not supporte by current Codacy revive v1.2.3 +#[rule.comment-spacings] +# disabled = true + +# Revive v1.2.3 is reporting incorrect empty lines. +[rule.empty-lines] + disabled = true + +# Fails to disable writers that actually cannot return errors. +[rule.unhandled-error] + disabled = true + +# Fails to restrict sufficiently in switches with numeric values. +[rule.add-constant] + disabled = true + +# Rule prevents intentional usage of similar variable names. +[rule.flag-parameter] + disabled = true + +# Rule prevents intentional usage of similar private method names. +[rule.confusing-naming] + disabled = true + +# Enables a more experienced cyclomatic complexity (we enabled a log of rules +# to counter-act the complexity trap). +[rule.cyclomatic] + arguments = [20] + +# Enables a more experienced cognitive complexity (we enabled a lot of rules +# to counter-act the complexity trap). +[rule.cognitive-complexity] + arguments = [20] + +# Limit line-length to increase readability. +[rule.line-length-limit] + arguments = [100] + +# We are a bit more relexed with function length consistent with funlen. +[rule.function-length] + arguments = [40,60] + +# Limit arguments of functions to the maximum understandable value. +[rule.argument-limit] + arguments = [6] + +# Limit results of functions to the maximum understandable value. +[rule.function-result-limit] + arguments = [4] + +# Raise the limit a bit to allow more complex package models. +[rule.max-public-structs] + arguments = [8] + +# I do not know what I'm doing here... +[rule.banned-characters] + arguments = ["Ω", "Σ", "σ"]