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..a66b498 --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,26 @@ +name: Go +on: [push, pull_request] +jobs: + test: + 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.20 + + - name: Build and tests + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + run: make --file=Makefile.base --trace all + + - name: Release and publish + run: make --file=Makefile.base --trace release publish + + - name: Send coverage report + uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: ./build/test-all.cover 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..960efc6 --- /dev/null +++ b/Makefile.base @@ -0,0 +1,1283 @@ +# === 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 lint-vuln +TARGETS_TEST ?= test-go test-all test-upload +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)*") +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}" && \ + 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 \ + echo "publish: go version $${TARGET}"; \ + curl --silent --show-error --fail --location \ + https://sum.golang.org/lookup/$${TARGET}; \ + 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),^$*$$); + +# Include custom targets to extend scripts. +ifneq ("$(wildcard Makefile.ext)","") + include Makefile.ext +endif + +## Custom: custom extension targets. 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..115d48a --- /dev/null +++ b/README.md @@ -0,0 +1,223 @@ +# 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 +using versions from [`ubuntu-20.04`][ubuntu-20.04] by the runtime environment: + +* [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]: +[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`]-project is optimized for Zalando in-house requirements and +adheres to the following conventions: + +1. All commands are provided by a `main.go` placed as usual under `cmd` using + the pattern `cmd//main.go`. It is also possible to have a `main.go` in + the root folder. + +2. All source code files and package names 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, 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..2f51157 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/tkrop/go-make + +go 1.20 + +require golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..252d9f0 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= diff --git a/main.go b/main.go new file mode 100644 index 0000000..919e778 --- /dev/null +++ b/main.go @@ -0,0 +1,289 @@ +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") +) + +func main() { + if trace { + LogInfo(os.Stderr, false) + LogCall(os.Stderr) + } else if slices.Contains(os.Args, "--version") { + LogInfo(os.Stderr, true) + 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 = ["Ω", "Σ", "σ"]