13 Commits
Author SHA1 Message Date
stefan 8b054914ea docs: prepare changelog for v0.0.5
continuous-integration/drone/tag Build is passing
- add CHANGELOG.md for next build

- document changes since v0.0.4

- add compare links for Unreleased and v0.0.5
2026-03-29 20:54:53 +02:00
stefan f5f980cfde chore: remove package step from CI pipeline
continuous-integration/drone/push Build is passing
2026-03-29 20:48:40 +02:00
stefan 9b0d844ec3 docs: add releasing process documentation
chore: update go.mod dependencies and versions

chore: update go.sum with new dependency versions

scripts: add coverage check script

scripts: add release notes generation script

test: refactor tests to use goconvey for assertions

test: enhance webserver tests with additional cases and goconvey

fix: improve error logging in web server start and stop methods
2026-03-29 20:47:30 +02:00
stefan d5660ba8ca chore: apply project standards templates 2026-03-29 19:29:18 +02:00
stefan d74b700691 NO-ISSUE: Add fix for vulnerabilty test
continuous-integration/drone/push Build is passing
2023-07-17 19:56:39 +02:00
stefan ae9b99321b NO-ISSUE: Fixed build
continuous-integration/drone/push Build is failing
2023-07-17 17:46:17 +00:00
stefan 153fc43e55 NO-ISSUE: Updated dependencies
continuous-integration/drone/push Build is failing
2023-07-17 19:33:16 +02:00
stefan 43ad8f8c5c Add badges
continuous-integration/drone/push Build is passing
2023-03-08 21:31:15 +01:00
stefan 89a200d4d8 Remove go image version
continuous-integration/drone/push Build is passing
2023-03-08 20:03:04 +00:00
stefan 0a663fe94a Updated build container
continuous-integration/drone/push Build is passing
2023-03-08 19:57:30 +00:00
stefan 29676ee524 Updated build container
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-03-08 19:44:58 +00:00
stefan 763d8492c4 Updated libraries
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-03-08 10:54:35 +01:00
stefan b4d5cf5ee6 Add vulnerability check for go
continuous-integration/drone/push Build is failing
2023-03-08 09:27:09 +01:00
22 changed files with 1396 additions and 429 deletions
+47 -12
View File
@@ -1,18 +1,53 @@
kind: pipeline kind: pipeline
type: docker type: docker
name: go-lib/micro name: go-lib/micro
platform:
arch: amd64
os: linux
steps:
- name: build
image: golang:1.18.0
commands:
- go get ./...
- go test ./...
trigger: trigger:
event: event:
- push - push
- cron - tag
- custom ref:
include:
- refs/heads/**
- refs/tags/v*
steps:
- name: test
image: golang:1.25.8
commands:
- go get ./...
- go vet ./...
- mkdir -p .build
- go test -v -coverprofile .build/coverage.out -coverpkg=./config,./web ./...
- go tool cover -func .build/coverage.out | tee .build/coverage.txt
- bash scripts/check-coverage.sh .build/coverage.out 80
- go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck -json ./... > .build/vulncheck.json
- name: release-notes
image: golang:1.25.8
commands:
- git fetch --tags --force || true
- bash scripts/generate-release-notes.sh
when:
event:
- tag
status:
- success
- name: release
image: plugins/gitea-release
settings:
base_url: https://scm.yoorie.de
api_key:
from_secret: gitea_token
files:
- .build/coverage.txt
- .build/release-notes.md
title: ${DRONE_TAG}
note_from_file: .build/release-notes.md
when:
event:
- tag
status:
- success
+16
View File
@@ -0,0 +1,16 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.go]
indent_style = tab
indent_size = 4
[*.md]
trim_trailing_whitespace = false
+17
View File
@@ -0,0 +1,17 @@
* text=auto
# Ensure LF for shell scripts and common source/docs files
*.sh text eol=lf
*.bash text eol=lf
*.zsh text eol=lf
*.go text eol=lf
*.mod text eol=lf
*.sum text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
Makefile text eol=lf
# Keep native Windows script formats
*.bat text eol=crlf
*.cmd text eol=crlf
+21
View File
@@ -0,0 +1,21 @@
# Git Hooks
This repository standard uses a project-local hooks directory:
- `.githooks/pre-commit`
Activate it once per repository:
```sh
git config core.hooksPath .githooks
```
The pre-commit hook validates for staged `.sh` files:
- executable bit in Git index (`100755`)
- LF line endings (no CRLF)
The pre-commit hook also validates staged `.md` files with `markdownlint`:
- no `markdownlint` errors or problems
- requires `markdownlint` CLI in PATH (for example via `npm install --global markdownlint-cli`)
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env sh
set -eu
failed=0
cr=$(printf '\r')
staged_shell_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.sh$' || true)
staged_markdown_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.md$' || true)
for file in $staged_shell_files; do
mode=$(git ls-files --stage -- "$file" | awk '{print $1}')
if [ "$mode" != "100755" ]; then
echo "ERROR: $file is not executable in Git index. Run: git add --chmod=+x $file" >&2
failed=1
fi
if git show ":$file" | grep -q "$cr"; then
echo "ERROR: $file contains CRLF in staged content. Use LF line endings." >&2
failed=1
fi
done
if [ -n "$staged_markdown_files" ]; then
if ! command -v markdownlint >/dev/null 2>&1; then
echo "ERROR: markdownlint is required to validate staged Markdown files (.md)." >&2
echo "Install with npm: npm install --global markdownlint-cli" >&2
failed=1
else
# Validate the staged markdown files currently present in the working tree.
# This keeps the hook simple and fast for standard project usage.
if ! markdownlint $staged_markdown_files; then
failed=1
fi
fi
fi
if [ "$failed" -ne 0 ]; then
echo "Pre-commit check failed." >&2
exit 1
fi
+11
View File
@@ -15,3 +15,14 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
.build/
bin/
dist/
tmp/
coverage/
coverage.out
*.coverprofile
.DS_Store
Thumbs.db
.vscode/
.idea/
+88
View File
@@ -0,0 +1,88 @@
# AGENTS.md
## Purpose
This file defines the default working agreement for AI coding agents and contributors in this repository.
## Documentation Language
Project language is English. All documentation, issues, pull requests, commit messages, and code comments should be written in English.
All newly created documentation in this project must be written in English.
All project documentation files must be stored under `docs/`, except `README.md` and `AGENTS.md`.
## Working Principles
1. Keep changes minimal and focused on the requested task.
2. Preserve existing public APIs unless a breaking change is explicitly requested.
3. Prefer clear, maintainable code over clever shortcuts.
4. Do not modify unrelated files.
5. Never add secrets, credentials, or tokens to the repository.
## Testing Expectations
1. Add or update tests for behavior changes.
2. Keep tests deterministic and fast.
3. Prefer table-driven tests where they improve readability.
4. Run relevant tests locally before finishing changes.
5. For Go projects, use `github.com/smartystreets/goconvey` as the standard test library.
## Build Artifacts and Reports
1. Builder logs and generated reports must be created under `.build/`.
2. The `.build/` directory must be excluded from version control via `.gitignore`.
## Git and Script Standards
1. Shell scripts (`*.sh`) must use LF line endings.
2. Shell scripts committed to the repository must be executable in Git index (mode `100755`).
3. When adding a new shell script, set execute permissions before commit: `git add --chmod=+x path/to/script.sh`.
## Git Bash Execution Defaults
1. Repository maintenance scripts are executed with Git Bash shell on Windows.
2. Default repository root is `~/git`.
3. The repository root can be overridden via `GO_GIT_ROOT`.
4. The standards repository defaults to `$ROOT_PATH/project-standards`.
5. The standards repository path can be overridden via `GO_PROJECT_STANDARDS`.
## Definition of Done (DoD)
### Purpose
The Definition of Done defines the minimum quality bar for every completed change in this repository.
### Mandatory Criteria
1. Tests
- Every code change is covered by tests where applicable.
- New functionality includes new tests.
- Bug fixes include at least one regression test.
- For Go projects, tests use `github.com/smartystreets/goconvey`.
- Automated test coverage is at least 80%.
1. Functional documentation
- Implemented functionality is documented.
- Public API-relevant changes are reflected in README and/or docs.
1. Documentation standards
- Documentation is written in English.
- Documentation files are placed under `docs/`.
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
### Technical Completion Criteria
1. Build and test status
- The project builds successfully.
- Relevant test commands run successfully.
1. No unresolved critical issues
- No new blocking errors are introduced.
- Known non-blocking warnings are acceptable only if unrelated to the change or documented.
1. SonarQube status
- No SonarQube errors are present.
1. Documentation structure
- Links to moved or newly added docs are valid.
- Documentation structure remains consistent with project rules.
### Review Checklist (Quick)
- [ ] Change is implemented and meets acceptance criteria.
- [ ] Tests were added/updated and pass.
- [ ] Go tests use `github.com/smartystreets/goconvey`.
- [ ] Automated test coverage is at least 80%.
- [ ] Functionality is documented.
- [ ] Documentation is in English.
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
- [ ] No SonarQube errors are present.
- [ ] No critical regressions found.
+28
View File
@@ -0,0 +1,28 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
## [v0.0.5] - 2026-03-29
### Changed
- CI pipeline no longer includes the package step.
### Added
- Release process documentation in [docs/RELEASING.md](docs/RELEASING.md).
### Maintenance
- Applied project standards templates and repository housekeeping updates.
## [v0.0.4] - 2026-03-29
- Previous release.
[Unreleased]: https://scm.yoorie.de/go-lib/micro/compare/v0.0.5...main
[v0.0.5]: https://scm.yoorie.de/go-lib/micro/compare/v0.0.4...v0.0.5
+17 -204
View File
@@ -1,208 +1,21 @@
Apache License The MIT License (MIT)
Version 2.0, January 2004 Copyright (c) 2015 Erik Davidson
http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, Permission is hereby granted, free of charge, to any person obtaining a copy
AND DISTRIBUTION 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:
1. Definitions. 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,
"License" shall mean the terms and conditions for use, reproduction, and distribution FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
as defined by Sections 1 through 9 of this document. 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.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct
or indirect, to cause the direction or management of such entity, whether
by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled object
code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that
is included in or attached to the work (an example is provided in the Appendix
below).
"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative
Works shall not include works that remain separable from, or merely link (or
bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative
Works thereof, that is intentionally submitted to Licensor for inclusion in
the Work by the copyright owner or by an individual or Legal Entity authorized
to submit on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication
sent to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor
for the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently incorporated
within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
Derivative Works of, publicly display, publicly perform, sublicense, and distribute
the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License,
each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section) patent
license to make, have made, use, offer to sell, sell, import, and otherwise
transfer the Work, where such license applies only to those patent claims
licensable by such Contributor that are necessarily infringed by their Contribution(s)
alone or by combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
that the Work or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses granted to You
under this License for that Work shall terminate as of the date such litigation
is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or
Derivative Works thereof in any medium, with or without modifications, and
in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy
of this License; and
(b) You must cause any modified files to carry prominent notices stating that
You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source
form of the Work, excluding those notices that do not pertain to any part
of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution,
then any Derivative Works that You distribute must include a readable copy
of the attribution notices contained within such NOTICE file, excluding those
notices that do not pertain to any part of the Derivative Works, in at least
one of the following places: within a NOTICE text file distributed as part
of the Derivative Works; within the Source form or documentation, if provided
along with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works
that You distribute, alongside or as an addendum to the NOTICE text from the
Work, provided that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction,
or distribution of Your modifications, or for any such Derivative Works as
a whole, provided Your use, reproduction, and distribution of the Work otherwise
complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any
Contribution intentionally submitted for inclusion in the Work by You to the
Licensor shall be under the terms and conditions of this License, without
any additional terms or conditions. Notwithstanding the above, nothing herein
shall supersede or modify the terms of any separate license agreement you
may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names,
trademarks, service marks, or product names of the Licensor, except as required
for reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to
in writing, Licensor provides the Work (and each Contributor provides its
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied, including, without limitation, any warranties
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
of using or redistributing the Work and assume any risks associated with Your
exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether
in tort (including negligence), contract, or otherwise, unless required by
applicable law (such as deliberate and grossly negligent acts) or agreed to
in writing, shall any Contributor be liable to You for damages, including
any direct, indirect, special, incidental, or consequential damages of any
character arising as a result of this License or out of the use or inability
to use the Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all other commercial
damages or losses), even if such Contributor has been advised of the possibility
of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work
or Derivative Works thereof, You may choose to offer, and charge a fee for,
acceptance of support, warranty, indemnity, or other liability obligations
and/or rights consistent with this License. However, in accepting such obligations,
You may act only on Your own behalf and on Your sole responsibility, not on
behalf of any other Contributor, and only if You agree to indemnify, defend,
and hold each Contributor harmless for any liability incurred by, or claims
asserted against, such Contributor by reason of your accepting any such warranty
or additional liability. END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own identifying
information. (Don't include the brackets!) The text should be enclosed in
the appropriate comment syntax for the file format. We also recommend that
a file or class name and description of purpose be included on the same "printed
page" as the copyright notice for easier identification within third-party
archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+186 -5
View File
@@ -1,7 +1,188 @@
# micro # go-lib/micro
yoorie Go micro service library
## Build
[![Build Status](https://drone.yoorie.de/api/badges/go-lib/micro/status.svg)](https://drone.yoorie.de/go-lib/micro) [![Build Status](https://drone.yoorie.de/api/badges/go-lib/micro/status.svg)](https://drone.yoorie.de/go-lib/micro)
[![Release](https://raster.shields.io/badge/dynamic/json.png?label=release&url=https://scm.yoorie.de/api/v1/repos/go-lib/micro/releases&query=$[0].tag_name)](https://scm.yoorie.de/go-lib/micro/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://scm.yoorie.de/go-lib/micro/src/branch/main/LICENSE)
Lightweight Go helper library for building microservices with:
- Opinionated HTTP/HTTPS server bootstrap around chi.
- Built-in health and readiness endpoints.
- Convention-based YAML config loading with defaults.
## Installation
```bash
go get scm.yoorie.de/go-lib/micro
```
## Packages
- [config/configfiles.go](config/configfiles.go): configuration discovery and loading.
- [web/server.go](web/server.go): HTTP/HTTPS server bootstrap and lifecycle.
- [web/health.go](web/health.go): health-check worker and endpoint payloads.
## Configuration Loading
The config package supports:
- Default values via struct tags.
- Loading from an explicit file path.
- Optional convention lookup based on an AppName method.
Lookup order for convention-based loading:
1. Executable directory with executable-name.yml
2. Executable directory with executable-name.yaml
3. User-local file in home directory under .myservice/config
4. Global config path returned by util.GetGlobalConfigurationFile(appName, "config")
Example:
```go
package main
import (
"log"
"scm.yoorie.de/go-lib/micro/config"
)
type ServiceConfig struct {
Port int `default:"7080" yaml:"port"`
Host string `default:"0.0.0.0" yaml:"host"`
}
// Optional: enables convention-based config file discovery.
func (c *ServiceConfig) AppName() string {
return "myservice"
}
func main() {
cfg := &ServiceConfig{}
// Option A: explicit file
if err := config.LoadConfigurationFromFile(cfg, "config.yml"); err != nil {
log.Fatal(err)
}
// Option B: automatic discovery using AppName()
// if err := config.LoadConfiguration(cfg); err != nil {
// log.Fatal(err)
// }
}
```
## Web Server
The web package wraps chi and starts:
- HTTP server when SslPort is not set.
- HTTPS server and separate HTTP health endpoint server when SslPort is set.
Default health endpoints:
- /health/healthz
- /health/readyz
Example:
```go
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"scm.yoorie.de/go-lib/micro/web"
)
type Message struct {
Text string `json:"text"`
}
func routes() *chi.Mux {
r := chi.NewRouter()
r.Get("/hello", func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, Message{Text: "hello"})
})
return r
}
func main() {
cfg := &web.WebServerConfiguration{
Host: "0.0.0.0",
Port: 7080,
}
server, err := web.NewWebServer(cfg)
if err != nil {
panic(err)
}
// Optional custom health function.
server.HealthCheck = func() (bool, string) {
return true, ""
}
server.Mount("/api", routes())
if err := server.Start(); err != nil {
panic(err)
}
// Block until interrupt.
server.Join()
}
```
## Development
Run unit tests:
```bash
go test ./...
```
Run vet:
```bash
go vet ./...
```
Run coverage like CI:
```bash
mkdir -p .build
go test -v -coverprofile .build/coverage.out -coverpkg=./config,./web ./...
go tool cover -func .build/coverage.out
bash scripts/check-coverage.sh .build/coverage.out 80
```
Run vulnerability scan:
```bash
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
```
## CI and Release
Drone pipeline is defined in [.drone.yml](.drone.yml) and includes:
- Tests
- Coverage gate (80%)
- go vet
- govulncheck
- Tag-based release notes and packaging
For the complete release workflow see [docs/RELEASING.md](docs/RELEASING.md).
## Project Docs
- [docs/DEFINITION_OF_DONE.md](docs/DEFINITION_OF_DONE.md)
- [docs/RELEASING.md](docs/RELEASING.md)
## License
MIT, see [LICENSE](LICENSE).
+45 -21
View File
@@ -1,9 +1,7 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
@@ -16,7 +14,10 @@ import (
func GetConfigurationFiles(appName string) []string { func GetConfigurationFiles(appName string) []string {
// First checking local config // First checking local config
executablePath, _ := os.Executable() executablePath, executableErr := os.Executable()
if executableErr != nil {
executablePath = os.Args[0]
}
fname := filepath.Base(executablePath) fname := filepath.Base(executablePath)
fext := filepath.Ext(executablePath) fext := filepath.Ext(executablePath)
fdir := filepath.Dir(executablePath) fdir := filepath.Dir(executablePath)
@@ -26,8 +27,11 @@ func GetConfigurationFiles(appName string) []string {
for _, ext := range ymlExts { for _, ext := range ymlExts {
fileNames = append(fileNames, filepath.Join(fdir, fnameWoExt+ext)) fileNames = append(fileNames, filepath.Join(fdir, fnameWoExt+ext))
} }
usr, _ := user.Current() homeDir := "."
fileNames = append(fileNames, filepath.Join(usr.HomeDir, "."+appName, "config")) if usr, userErr := user.Current(); userErr == nil {
homeDir = usr.HomeDir
}
fileNames = append(fileNames, filepath.Join(homeDir, "."+appName, "config"))
// Check for global config // Check for global config
fileNames = append(fileNames, util.GetGlobalConfigurationFile(appName, "config")) fileNames = append(fileNames, util.GetGlobalConfigurationFile(appName, "config"))
return fileNames return fileNames
@@ -48,6 +52,33 @@ func LoadConfiguration[T any](config T) error {
return LoadConfigurationFromFile(config, "") return LoadConfigurationFromFile(config, "")
} }
func resolveConfigFile[T any](config T, configFile string) (string, error) {
if configFile != "" {
if !util.FileExists(configFile) {
return "", fmt.Errorf("given configuration file %s cannot be found", configFile)
}
return configFile, nil
}
appName := getAppNameFromConfig(config)
if appName == "" {
return "", nil
}
return GetConfigurationFile(appName), nil
}
func getAppNameFromConfig[T any](config T) string {
method := reflect.ValueOf(config).MethodByName("AppName")
if !method.IsValid() {
return ""
}
results := method.Call(nil)
if len(results) == 0 {
return ""
}
return results[0].String()
}
func IsStructPointer[T any](config T) bool { func IsStructPointer[T any](config T) bool {
value := reflect.ValueOf(config) value := reflect.ValueOf(config)
return value.Type().Kind() == reflect.Pointer && reflect.Indirect(value).Type().Kind() == reflect.Struct return value.Type().Kind() == reflect.Pointer && reflect.Indirect(value).Type().Kind() == reflect.Struct
@@ -56,32 +87,25 @@ func IsStructPointer[T any](config T) bool {
func LoadConfigurationFromFile[T any](config T, configFile string) error { func LoadConfigurationFromFile[T any](config T, configFile string) error {
if !IsStructPointer(config) { if !IsStructPointer(config) {
genericType := reflect.TypeOf(config) genericType := reflect.TypeOf(config)
return errors.New(fmt.Sprintf("Type: %s.%s is not a struct pointer", genericType.PkgPath(), genericType.Name())) return fmt.Errorf("type: %s.%s is not a struct pointer", genericType.PkgPath(), genericType.Name())
} }
defaults.Set(config) defaults.Set(config)
if configFile != "" {
if !util.FileExists(configFile) { resolvedConfigFile, err := resolveConfigFile(config, configFile)
return errors.New(fmt.Sprintf("given configuration file %s cannot be found", configFile)) if err != nil {
} return err
} else {
if method := reflect.ValueOf(config).MethodByName("appName"); !method.IsNil() {
result := method.String()
if result != "" {
configFile = GetConfigurationFile(result)
}
}
} }
if configFile == "" { if resolvedConfigFile == "" {
return nil return nil
} }
ymldata, err := ioutil.ReadFile(configFile) ymldata, err := os.ReadFile(resolvedConfigFile)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("cannot read configuration file %s. %v", configFile, err)) return fmt.Errorf("cannot read configuration file %s: %w", resolvedConfigFile, err)
} }
err = yaml.Unmarshal(ymldata, config) err = yaml.Unmarshal(ymldata, config)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("error unmarshalling configuration data: %v", err)) return fmt.Errorf("error unmarshalling configuration data: %w", err)
} }
return nil return nil
} }
+52
View File
@@ -0,0 +1,52 @@
# Definition of Done (DoD)
## Purpose
This Definition of Done defines the minimum quality bar for every completed change in this repository.
## Mandatory Criteria
1. Tests
- Every code change is covered by tests where applicable.
- New functionality includes new tests.
- Bug fixes include at least one regression test.
- For Go projects, tests use `github.com/smartystreets/goconvey`.
- Automated test coverage is at least 80%.
1. Functional documentation
- Implemented functionality is documented.
- Public API-relevant changes are reflected in README and/or docs.
1. Documentation standards
- Documentation is written in English.
- Documentation files are placed under `docs/`.
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
## Technical Completion Criteria
1. Build and test status
- The project builds successfully.
- Relevant test commands run successfully.
1. No unresolved critical issues
- No new blocking errors are introduced.
- Known non-blocking warnings are acceptable only if unrelated to the change or documented.
1. SonarQube status
- No SonarQube errors are present.
1. Documentation links and structure
- Links to moved or newly added docs are valid.
- Documentation structure remains consistent with project rules.
## Review Checklist (Quick)
- [ ] Change is implemented and meets acceptance criteria.
- [ ] Tests were added/updated and pass.
- [ ] Go tests use `github.com/smartystreets/goconvey`.
- [ ] Automated test coverage is at least 80%.
- [ ] Functionality is documented.
- [ ] Documentation is in English.
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
- [ ] No SonarQube errors are present.
- [ ] No critical regressions found.
+115
View File
@@ -0,0 +1,115 @@
# Releasing go-lib/micro
This document describes the process for creating a release of the
`go-lib/micro` library.
## Overview
Releases in this project are managed via Git tags. When you push a
tag matching the pattern `v*` (for example, `v0.2.0`), the Drone
pipeline automatically:
1. Runs quality checks (tests, coverage gate, vet, vulnerability scan)
2. Generates release notes from commits
3. Creates a source archive (`sources.tar.gz`)
4. Publishes a release to Gitea with attached artifacts
## Versioning
This library follows semantic versioning: `MAJOR.MINOR.PATCH`
(for example, `v1.2.3`).
- `v1.0.0`: breaking API changes
- `v1.1.0`: backward-compatible features
- `v1.0.1`: backward-compatible fixes
## Prerequisites
Before creating a release, ensure:
1. Working tree is clean (`git status`)
2. Tests pass locally (`go test ./...`)
3. Coverage is at least 80%
4. Dependencies are tidy (`go mod tidy`)
5. Documentation is up to date (README/docs)
## Create a Release
### 1. Prepare (optional)
Update docs, examples, or changelog if needed:
```bash
git add README.md docs/
git commit -m "docs: prepare release"
```
### 2. Create tag
```bash
git tag -a v0.2.0 -m "Release v0.2.0"
```
### 3. Push tag
```bash
git push origin v0.2.0
```
### 4. Verify pipeline and release
After pushing the tag, verify in Drone and Gitea:
- Pipeline succeeded for tag build
- Gitea release exists with artifacts:
- `.build/coverage.txt`
- `.build/sources.tar.gz`
- `.build/release-notes.md`
## Install Released Version
```bash
# latest
go get scm.yoorie.de/go-lib/micro
# specific
go get scm.yoorie.de/go-lib/micro@v0.2.0
```
## Troubleshooting
### Coverage below threshold
Run locally and inspect uncovered code:
```bash
go test -v -coverprofile .build/coverage.out ./...
go tool cover -func .build/coverage.out
```
### Release step fails
Common causes:
- Missing Drone secret `gitea_token`
- Tag does not match `v*`
- Earlier pipeline step failed
### Wrong tag pushed
```bash
git tag -d v0.2.0
git push origin :refs/tags/v0.2.0
```
Then create and push the corrected tag.
## Release Checklist
- [ ] Tests pass locally
- [ ] Coverage is at least 80%
- [ ] `go vet ./...` passes
- [ ] `govulncheck ./...` is clean or reviewed
- [ ] Tag `vX.Y.Z` created and pushed
- [ ] Drone pipeline succeeded
- [ ] Gitea release contains expected artifacts
+17 -18
View File
@@ -1,32 +1,31 @@
module scm.yoorie.de/go-lib/micro module scm.yoorie.de/go-lib/micro
go 1.18 go 1.20
require ( require (
github.com/creasty/defaults v1.6.0 github.com/creasty/defaults v1.7.0
github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.2 github.com/go-chi/render v1.0.3
github.com/stretchr/testify v1.8.2 github.com/smartystreets/goconvey v1.8.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
scm.yoorie.de/go-lib/certs v0.0.1 scm.yoorie.de/go-lib/certs v0.0.3
scm.yoorie.de/go-lib/gelf v0.0.1 scm.yoorie.de/go-lib/gelf v0.0.3
scm.yoorie.de/go-lib/util v0.0.4 scm.yoorie.de/go-lib/util v0.6.0
) )
require ( require (
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/kr/pretty v0.1.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect
github.com/sergi/go-diff v1.2.0 // indirect github.com/smarty/assertions v1.15.0 // indirect
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect golang.org/x/crypto v0.11.0 // indirect
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect golang.org/x/sys v0.10.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
gopkg.in/aphistic/golf.v0 v0.0.0-20180712155816-02c07f170c5a // indirect gopkg.in/aphistic/golf.v0 v0.0.0-20180712155816-02c07f170c5a // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
) )
+56 -46
View File
@@ -1,71 +1,81 @@
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/aphistic/sweet v0.3.0 h1:xZTMfCoMsjWubPNxOBODluBC4qfGP0CdRJ88jon46XE= github.com/aphistic/sweet v0.3.0 h1:xZTMfCoMsjWubPNxOBODluBC4qfGP0CdRJ88jon46XE=
github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BSc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creasty/defaults v1.6.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA=
github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.27.2 h1:SKU0CXeKE/WVgIV1T61kSa3+IRE8Ekrv9rdXDwwTqnY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/aphistic/golf.v0 v0.0.0-20180712155816-02c07f170c5a h1:34vqlRjuZiE9c8eHsuZ9nn+GbcimFpvGUEmW+vyfhG8= gopkg.in/aphistic/golf.v0 v0.0.0-20180712155816-02c07f170c5a h1:34vqlRjuZiE9c8eHsuZ9nn+GbcimFpvGUEmW+vyfhG8=
gopkg.in/aphistic/golf.v0 v0.0.0-20180712155816-02c07f170c5a/go.mod h1:fvTxI2ZW4gO1d+4q4VCKOo+ANBs4gPN3IW00MlCumKc= gopkg.in/aphistic/golf.v0 v0.0.0-20180712155816-02c07f170c5a/go.mod h1:fvTxI2ZW4gO1d+4q4VCKOo+ANBs4gPN3IW00MlCumKc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= scm.yoorie.de/go-lib/certs v0.0.3 h1:a7wCAJQj1X+NxwS8r2o49m29oBh1/YvqkUsMQR2GUyk=
scm.yoorie.de/go-lib/certs v0.0.1 h1:4FwWaIjtMQbhpr75kVD7Ymvz5fwpeM5kt1jrjrIHFsg= scm.yoorie.de/go-lib/certs v0.0.3/go.mod h1:2z4kefIECDXFJrMnRRFO2tax4Udy2WeABkLN3kWgItg=
scm.yoorie.de/go-lib/certs v0.0.1/go.mod h1:CYe8HvRaply1NRTQ9YnllvdijoZwNR4yXGhrN1L9BhM= scm.yoorie.de/go-lib/gelf v0.0.3 h1:3tZQdDQNJisyVQRyY4YfUYwp3dF+s7NlI12noXiiJdg=
scm.yoorie.de/go-lib/gelf v0.0.1 h1:VcCUihF1zyOiDfPg2NWLyaBoJjundua624RBAPJx/vU= scm.yoorie.de/go-lib/gelf v0.0.3/go.mod h1:zZrZ4zGuKFYh8AIeA0nPHWXWHrLoNiKIQ/KrSQhky+s=
scm.yoorie.de/go-lib/gelf v0.0.1/go.mod h1:p4HaHQX4mcgclzYB8nomFHvYLLdz64yr7MYlV5+Oz/I= scm.yoorie.de/go-lib/util v0.6.0 h1:74Z4vALoMNgsSeETTIC77qTVYoE0QMafsJhCQqw7Y+g=
scm.yoorie.de/go-lib/util v0.0.4 h1:0gyUM4689jwrwzeQFAY8mF74YO68apznIhSxhYI3pxA= scm.yoorie.de/go-lib/util v0.6.0/go.mod h1:t9TLwQzr8F1g+SLn9llFbGS0a5hrP+M89IrrOCVpszU=
scm.yoorie.de/go-lib/util v0.0.4/go.mod h1:ckOH3jMooqDIaksOVCFIx/2S8aTDlXuBYfIKsLwznIU=
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Check test coverage against minimum threshold.
set -euo pipefail
COVERAGE_FILE="${1:-.build/coverage.out}"
MIN_COVERAGE="${2:-80}"
if [ ! -f "$COVERAGE_FILE" ]; then
echo "Error: coverage file not found: $COVERAGE_FILE"
exit 1
fi
# Extract total coverage percentage from go tool cover output.
COVERAGE=$(go tool cover -func "$COVERAGE_FILE" | awk '/^total:/ { match($0, /[0-9.]+%/); print substr($0, RSTART, RLENGTH-1) }')
if [ -z "$COVERAGE" ]; then
echo "Error: failed to parse coverage from $COVERAGE_FILE"
exit 1
fi
echo "Total coverage: ${COVERAGE}%"
# Compare as integer part to keep shell arithmetic simple and portable.
COVERAGE_INT=${COVERAGE%.*}
if [ "$COVERAGE_INT" -lt "$MIN_COVERAGE" ]; then
echo "Coverage ${COVERAGE}% is below minimum ${MIN_COVERAGE}%"
exit 1
fi
echo "Coverage check passed"
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
# Generate release notes from commits since the previous tag.
set -euo pipefail
CURRENT_TAG="${1:-${DRONE_TAG:-}}"
OUTPUT_FILE="${2:-.build/release-notes.md}"
if [ -z "$CURRENT_TAG" ]; then
echo "Error: current tag not provided"
exit 1
fi
# In CI, the tag name may be available but not present as a local ref.
CURRENT_REF="$CURRENT_TAG"
if ! git rev-parse --verify -q "${CURRENT_REF}^{commit}" >/dev/null; then
CURRENT_REF="${DRONE_COMMIT:-HEAD}"
fi
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${CURRENT_REF}^" 2>/dev/null || echo "")
mkdir -p "$(dirname "$OUTPUT_FILE")"
{
echo "# Release $CURRENT_TAG"
echo
if [ -n "$PREVIOUS_TAG" ]; then
echo "Changes since $PREVIOUS_TAG:"
echo
git log "$PREVIOUS_TAG..$CURRENT_REF" --pretty=format:"%s" | while read -r commit; do
if [[ $commit =~ ^([a-z]+)(\(.+\))?:\ (.+)$ ]]; then
TYPE="${BASH_REMATCH[1]}"
SCOPE="${BASH_REMATCH[2]}"
MESSAGE="${BASH_REMATCH[3]}"
case "$TYPE" in
feat)
echo "- Feature${SCOPE}: $MESSAGE" ;;
fix)
echo "- Fix${SCOPE}: $MESSAGE" ;;
docs)
echo "- Docs${SCOPE}: $MESSAGE" ;;
ci)
echo "- CI${SCOPE}: $MESSAGE" ;;
test)
echo "- Test${SCOPE}: $MESSAGE" ;;
refactor)
echo "- Refactor${SCOPE}: $MESSAGE" ;;
perf)
echo "- Performance${SCOPE}: $MESSAGE" ;;
*)
echo "- $commit" ;;
esac
else
echo "- $commit"
fi
done
else
echo "Initial release"
echo
git log "$CURRENT_REF" --pretty=format:"- %s" | head -20
fi
echo
echo "---"
if [ -n "$PREVIOUS_TAG" ]; then
echo "See all changes: https://scm.yoorie.de/git/go-lib/micro/compare/$PREVIOUS_TAG...$CURRENT_TAG"
else
echo "See all changes: https://scm.yoorie.de/git/go-lib/micro/releases/tag/$CURRENT_TAG"
fi
} > "$OUTPUT_FILE"
echo "Release notes generated: $OUTPUT_FILE"
cat "$OUTPUT_FILE"
+274
View File
@@ -0,0 +1,274 @@
#!/usr/bin/env sh
set -eu
ROOT_PATH="${GO_GIT_ROOT:-$HOME/git}"
STANDARDS_REPO=""
STANDARDS_REPO_SET=0
EXCLUDE_NAME="project-standards"
CHECK_ONLY=0
WATCH=0
INTERVAL=60
usage() {
cat <<'EOF'
Usage:
reconcile-project-standards.sh [options]
Options:
--root <path> Root folder containing repositories
--standards-repo <path> Path to project-standards repository
--exclude <name> First-level directory name to skip (default: project-standards)
--check-only Check drift only, do not update files
--watch Continuously scan and reconcile
--interval <seconds> Watch interval in seconds (default: 60)
-h, --help Show this help
Environment:
GO_GIT_ROOT Overrides default root path (default: ~/git)
GO_PROJECT_STANDARDS Overrides standards repository path
Default resolution:
ROOT_PATH defaults to GO_GIT_ROOT or ~/git.
STANDARDS_REPO defaults to GO_PROJECT_STANDARDS or $ROOT_PATH/project-standards.
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--root)
ROOT_PATH=$2
shift 2
;;
--standards-repo)
STANDARDS_REPO=$2
STANDARDS_REPO_SET=1
shift 2
;;
--exclude)
EXCLUDE_NAME=$2
shift 2
;;
--check-only)
CHECK_ONLY=1
shift
;;
--watch)
WATCH=1
shift
;;
--interval)
INTERVAL=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done
if [ "$STANDARDS_REPO_SET" -ne 1 ]; then
if [ -n "${GO_PROJECT_STANDARDS:-}" ]; then
STANDARDS_REPO="$GO_PROJECT_STANDARDS"
else
STANDARDS_REPO="$ROOT_PATH/project-standards"
fi
fi
if [ "$INTERVAL" -lt 5 ]; then
echo "interval must be >= 5" >&2
exit 1
fi
AGENTS_TEMPLATE="$STANDARDS_REPO/templates/AGENTS.base.md"
DOD_TEMPLATE="$STANDARDS_REPO/templates/DEFINITION_OF_DONE.base.md"
GITIGNORE_TEMPLATE="$STANDARDS_REPO/templates/.gitignore.base"
GITATTRIBUTES_TEMPLATE="$STANDARDS_REPO/templates/.gitattributes.base"
EDITORCONFIG_TEMPLATE="$STANDARDS_REPO/templates/.editorconfig.base"
PRECOMMIT_TEMPLATE="$STANDARDS_REPO/templates/pre-commit.base.sh"
HOOKS_README_TEMPLATE="$STANDARDS_REPO/templates/.githooks.README.base.md"
if [ ! -f "$AGENTS_TEMPLATE" ]; then
echo "AGENTS template not found: $AGENTS_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$DOD_TEMPLATE" ]; then
echo "DoD template not found: $DOD_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$GITIGNORE_TEMPLATE" ]; then
echo "gitignore template not found: $GITIGNORE_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$GITATTRIBUTES_TEMPLATE" ]; then
echo "gitattributes template not found: $GITATTRIBUTES_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$EDITORCONFIG_TEMPLATE" ]; then
echo "editorconfig template not found: $EDITORCONFIG_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$PRECOMMIT_TEMPLATE" ]; then
echo "pre-commit hook template not found: $PRECOMMIT_TEMPLATE" >&2
exit 1
fi
if [ ! -f "$HOOKS_README_TEMPLATE" ]; then
echo "hooks readme template not found: $HOOKS_README_TEMPLATE" >&2
exit 1
fi
hash_or_missing() {
path=$1
if [ ! -f "$path" ]; then
printf "%s" "__MISSING__"
return 0
fi
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$path" | awk '{print $1}'
else
shasum -a 256 "$path" | awk '{print $1}'
fi
}
ensure_file() {
template=$1
target=$2
template_hash=$(hash_or_missing "$template")
target_hash=$(hash_or_missing "$target")
if [ "$template_hash" = "$target_hash" ]; then
printf "%s" "ok"
return 0
fi
if [ "$CHECK_ONLY" -eq 1 ]; then
printf "%s" "drift"
return 0
fi
mkdir -p "$(dirname "$target")"
cp "$template" "$target"
printf "%s" "updated"
}
ensure_gitignore_entries_from_template() {
template_path=$1
gitignore_path=$2
missing=0
if [ ! -f "$gitignore_path" ]; then
if [ "$CHECK_ONLY" -eq 1 ]; then
printf "%s" "drift"
return 0
fi
: > "$gitignore_path"
fi
while IFS= read -r entry; do
entry=$(printf '%s' "$entry" | tr -d '\r')
[ -n "$entry" ] || continue
case "$entry" in
\#*)
continue
;;
esac
if tr -d '\r' < "$gitignore_path" | grep -Fqx "$entry"; then
continue
fi
missing=1
if [ "$CHECK_ONLY" -ne 1 ]; then
printf '%s\n' "$entry" >> "$gitignore_path"
fi
done < "$template_path"
if [ "$missing" -eq 0 ]; then
printf "%s" "ok"
return 0
fi
if [ "$CHECK_ONLY" -eq 1 ]; then
printf "%s" "drift"
else
printf "%s" "updated"
fi
}
run_once() {
scanned=0
updated=0
drift=0
for repo in "$ROOT_PATH"/*; do
[ -d "$repo" ] || continue
name=$(basename "$repo")
[ "$name" = "$EXCLUDE_NAME" ] && continue
scanned=$((scanned + 1))
agents_target="$repo/AGENTS.md"
dod_target="$repo/docs/DEFINITION_OF_DONE.md"
gitattributes_target="$repo/.gitattributes"
editorconfig_target="$repo/.editorconfig"
precommit_target="$repo/.githooks/pre-commit"
hooks_readme_target="$repo/.githooks/README.md"
gitignore_target="$repo/.gitignore"
agents_state=$(ensure_file "$AGENTS_TEMPLATE" "$agents_target")
dod_state=$(ensure_file "$DOD_TEMPLATE" "$dod_target")
gitattributes_state=$(ensure_file "$GITATTRIBUTES_TEMPLATE" "$gitattributes_target")
editorconfig_state=$(ensure_file "$EDITORCONFIG_TEMPLATE" "$editorconfig_target")
precommit_state=$(ensure_file "$PRECOMMIT_TEMPLATE" "$precommit_target")
hooks_readme_state=$(ensure_file "$HOOKS_README_TEMPLATE" "$hooks_readme_target")
gitignore_state=$(ensure_gitignore_entries_from_template "$GITIGNORE_TEMPLATE" "$gitignore_target")
if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ] || [ "$gitattributes_state" = "updated" ] || [ "$editorconfig_state" = "updated" ] || [ "$precommit_state" = "updated" ] || [ "$hooks_readme_state" = "updated" ] || [ "$gitignore_state" = "updated" ]; then
updated=$((updated + 1))
echo "UPDATED: $repo"
continue
fi
if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ] || [ "$gitattributes_state" = "drift" ] || [ "$editorconfig_state" = "drift" ] || [ "$precommit_state" = "drift" ] || [ "$hooks_readme_state" = "drift" ] || [ "$gitignore_state" = "drift" ]; then
drift=$((drift + 1))
echo "DRIFT: $repo"
continue
fi
echo "OK: $repo"
done
echo "Summary -> scanned=$scanned, updated=$updated, drift=$drift"
if [ "$CHECK_ONLY" -eq 1 ] && [ "$drift" -gt 0 ]; then
return 1
fi
return 0
}
if [ "$WATCH" -eq 1 ]; then
while :; do
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Reconciling project standards..."
run_once || true
sleep "$INTERVAL"
done
else
run_once
fi
+75 -57
View File
@@ -3,7 +3,7 @@ package test
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" . "github.com/smartystreets/goconvey/convey"
cfg "scm.yoorie.de/go-lib/micro/config" cfg "scm.yoorie.de/go-lib/micro/config"
) )
@@ -13,70 +13,88 @@ type MyConfig struct {
Value2 int `default:"8080" yaml:"value2"` Value2 int `default:"8080" yaml:"value2"`
} }
func (config *MyConfig) appName() string { func (config *MyConfig) AppName() string {
return "myconfig" return "myconfig"
} }
func TestIsStructPointerStruct(t *testing.T) { func TestIsStructPointer(t *testing.T) {
assert.False(t, cfg.IsStructPointer(MyConfig{})) Convey("IsStructPointer", t, func() {
} Convey("returns false for struct value", func() {
func TestIsStructPointerString(t *testing.T) { So(cfg.IsStructPointer(MyConfig{}), ShouldBeFalse)
assert.False(t, cfg.IsStructPointer("Bar")) })
} Convey("returns false for string", func() {
So(cfg.IsStructPointer("Bar"), ShouldBeFalse)
func TestIsStructPointerSimpleArray(t *testing.T) { })
assert.False(t, cfg.IsStructPointer([2]int{292, 2})) Convey("returns false for simple array", func() {
} So(cfg.IsStructPointer([2]int{292, 2}), ShouldBeFalse)
})
func TestIsStructPointerInt(t *testing.T) { Convey("returns false for int", func() {
assert.False(t, cfg.IsStructPointer(12)) So(cfg.IsStructPointer(12), ShouldBeFalse)
} })
Convey("returns false for bool", func() {
func TestIsStructPointerBool(t *testing.T) { So(cfg.IsStructPointer(false), ShouldBeFalse)
assert.False(t, cfg.IsStructPointer(false)) })
} Convey("returns true for struct pointer", func() {
So(cfg.IsStructPointer(&MyConfig{}), ShouldBeTrue)
func TestIsStructPointerStructPointer(t *testing.T) { })
assert.True(t, cfg.IsStructPointer(&MyConfig{})) Convey("returns false for struct pointer array", func() {
} cfg1 := &MyConfig{Value: "Foo"}
cfg2 := &MyConfig{Value: "Bar"}
func TestIsStructPointerStructPointerArray(t *testing.T) { array := [2]*MyConfig{cfg1, cfg2}
cfg1 := &MyConfig{ So(cfg.IsStructPointer(array), ShouldBeFalse)
Value: "Foo", })
} })
cfg2 := &MyConfig{
Value: "Bar",
}
array := [2]*MyConfig{cfg1, cfg2}
assert.False(t, cfg.IsStructPointer(array))
}
func TestLoadConfigurationFromFileWithNoPointerInvalidType(t *testing.T) {
myConfig := MyConfig{}
err := cfg.LoadConfigurationFromFile(myConfig, "testdata/config.yml")
assert.NotNil(t, err)
} }
func TestLoadConfigurationFromFile(t *testing.T) { func TestLoadConfigurationFromFile(t *testing.T) {
myConfig := &MyConfig{} Convey("LoadConfigurationFromFile", t, func() {
assert.Equal(t, "myconfig", myConfig.appName()) Convey("returns error for non-pointer type", func() {
cfg.LoadConfigurationFromFile(myConfig, "testdata/config.yml") err := cfg.LoadConfigurationFromFile(MyConfig{}, "testdata/config.yml")
assert.Equal(t, "Foo", myConfig.Value) So(err, ShouldNotBeNil)
assert.Equal(t, true, myConfig.Value1) })
assert.Equal(t, 8080, myConfig.Value2) Convey("loads config from file successfully", func() {
} myConfig := &MyConfig{}
func TestLoadConfigurationFromFileNotExist(t *testing.T) { err := cfg.LoadConfigurationFromFile(myConfig, "testdata/config.yml")
myConfig := &MyConfig{} So(err, ShouldBeNil)
assert.Equal(t, "myconfig", myConfig.appName()) So(myConfig.Value, ShouldEqual, "Foo")
err := cfg.LoadConfigurationFromFile(myConfig, "testdata/config1.yml") So(myConfig.Value1, ShouldBeTrue)
assert.NotNil(t, err) So(myConfig.Value2, ShouldEqual, 8080)
})
Convey("returns error when config file does not exist", func() {
myConfig := &MyConfig{}
err := cfg.LoadConfigurationFromFile(myConfig, "testdata/config1.yml")
So(err, ShouldNotBeNil)
})
})
} }
func TestGetConfigurationFiles(t *testing.T) { func TestGetConfigurationFiles(t *testing.T) {
fileNames := cfg.GetConfigurationFiles("myapp") Convey("GetConfigurationFiles returns 4 candidate paths", t, func() {
assert.Equal(t, 4, len(fileNames)) fileNames := cfg.GetConfigurationFiles("myapp")
So(len(fileNames), ShouldEqual, 4)
})
}
for _, fileName := range fileNames { func TestGetConfigurationFile(t *testing.T) {
t.Log(fileName) Convey("GetConfigurationFile", t, func() {
} Convey("returns empty string when no matching config file exists", func() {
result := cfg.GetConfigurationFile("nonexistentapp_xyz_123")
So(result, ShouldEqual, "")
})
})
}
func TestLoadConfiguration(t *testing.T) {
Convey("LoadConfiguration", t, func() {
Convey("returns error for non-pointer type", func() {
err := cfg.LoadConfiguration(MyConfig{})
So(err, ShouldNotBeNil)
})
Convey("returns nil for struct pointer with no config file found", func() {
myConfig := &MyConfig{}
err := cfg.LoadConfiguration(myConfig)
So(err, ShouldBeNil)
So(myConfig.Value, ShouldEqual, "Bar")
})
})
} }
+165 -46
View File
@@ -5,15 +5,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os" "os"
"testing" "testing"
"time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/stretchr/testify/assert" . "github.com/smartystreets/goconvey/convey"
"scm.yoorie.de/go-lib/micro/web" "scm.yoorie.de/go-lib/micro/web"
"scm.yoorie.de/go-lib/util" "scm.yoorie.de/go-lib/util"
) )
@@ -24,6 +24,12 @@ var (
sslPort int sslPort int
) )
const (
readyMessage = "service is ready"
aMessage = "A message"
testFailureMsg = "test failure"
)
type MyData struct { type MyData struct {
Message string `json:"message"` Message string `json:"message"`
Count int `json:"count"` Count int `json:"count"`
@@ -32,7 +38,7 @@ type MyData struct {
func MyTestEndpoint(w http.ResponseWriter, r *http.Request) { func MyTestEndpoint(w http.ResponseWriter, r *http.Request) {
internalCount++ internalCount++
render.JSON(w, r, &MyData{ render.JSON(w, r, &MyData{
Message: "A message", Message: aMessage,
Count: internalCount, Count: internalCount,
}) })
} }
@@ -91,7 +97,9 @@ func TestMain(m *testing.M) {
panic(err) panic(err)
} }
server.Mount("/api", CreateTestRouter()) server.Mount("/api", CreateTestRouter())
server.Start() if err = server.Start(); err != nil {
panic(err)
}
// Allow insecure calls to https // Allow insecure calls to https
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
@@ -101,56 +109,167 @@ func TestMain(m *testing.M) {
os.Exit(exitVal) os.Exit(exitVal)
} }
func TestNewWebServerNilConfig(t *testing.T) {
Convey("NewWebServer with nil config returns error", t, func() {
server, err := web.NewWebServer(nil)
So(err, ShouldNotBeNil)
So(server, ShouldBeNil)
})
}
func TestGetBindings(t *testing.T) {
Convey("WebServerConfiguration.GetBindings", t, func() {
Convey("returns only http binding when SSL port is 0", func() {
config := &web.WebServerConfiguration{Port: 8080}
So(config.GetBindings(), ShouldEqual, "http://0.0.0.0:8080")
})
Convey("returns https and http bindings when SSL port is set", func() {
config := &web.WebServerConfiguration{Port: 8080, SslPort: 8443}
So(config.GetBindings(), ShouldEqual, "https://0.0.0.0:8443,http://0.0.0.0:8080")
})
})
}
func TestBuildHostAddress(t *testing.T) {
Convey("WebServerConfiguration.BuildHostAddress", t, func() {
Convey("uses 0.0.0.0 when host is empty", func() {
config := &web.WebServerConfiguration{Port: 8080, SslPort: 8443}
So(config.BuildHostAddress(false), ShouldEqual, "0.0.0.0:8080")
So(config.BuildHostAddress(true), ShouldEqual, "0.0.0.0:8443")
})
Convey("uses configured host when set", func() {
config := &web.WebServerConfiguration{Host: "192.168.1.1", Port: 8080, SslPort: 8443}
So(config.BuildHostAddress(false), ShouldEqual, "192.168.1.1:8080")
So(config.BuildHostAddress(true), ShouldEqual, "192.168.1.1:8443")
})
})
}
func TestStartWithNoMounts(t *testing.T) {
Convey("Start returns error when no mounts are registered", t, func() {
port, err := GetFreePort()
So(err, ShouldBeNil)
config := &web.WebServerConfiguration{Port: port}
server, err := web.NewWebServer(config)
So(err, ShouldBeNil)
err = server.Start()
So(err, ShouldNotBeNil)
})
}
func TestNonSSLServer(t *testing.T) {
Convey("Non-SSL web server starts, serves requests, and stops", t, func() {
port, err := GetFreePort()
So(err, ShouldBeNil)
config := &web.WebServerConfiguration{Port: port}
server, err := web.NewWebServer(config)
So(err, ShouldBeNil)
server.Mount("/api", CreateTestRouter())
err = server.Start()
So(err, ShouldBeNil)
time.Sleep(100 * time.Millisecond)
uri := fmt.Sprintf("http://localhost:%d/health/readyz", port)
resp, err := http.Get(uri)
So(err, ShouldBeNil)
So(resp.StatusCode, ShouldEqual, http.StatusOK)
body, _ := io.ReadAll(resp.Body)
healthData := web.HealthData{}
err = json.Unmarshal(body, &healthData)
So(err, ShouldBeNil)
So(healthData.Message, ShouldEqual, readyMessage)
server.Stop()
})
}
func TestUnhealthyServer(t *testing.T) {
Convey("Server with failing HealthCheck returns 503 from healthz endpoint", t, func() {
port, err := GetFreePort()
So(err, ShouldBeNil)
config := &web.WebServerConfiguration{Port: port}
server, err := web.NewWebServer(config)
So(err, ShouldBeNil)
server.HealthCheck = func() (bool, string) { return false, testFailureMsg }
server.Mount("/api", CreateTestRouter())
err = server.Start()
So(err, ShouldBeNil)
time.Sleep(100 * time.Millisecond)
uri := fmt.Sprintf("http://localhost:%d/health/healthz", port)
resp, err := http.Get(uri)
So(err, ShouldBeNil)
So(resp.StatusCode, ShouldEqual, http.StatusServiceUnavailable)
server.Stop()
})
}
func TestReady(t *testing.T) { func TestReady(t *testing.T) {
uri := getServerURL(false, "/readyz") Convey("HTTP ready endpoint returns 200 with ready message", t, func() {
resp, err := http.Get(uri) uri := getServerURL(false, "/readyz")
assert.Nil(t, err) resp, err := http.Get(uri)
assert.Equal(t, http.StatusOK, resp.StatusCode) So(err, ShouldBeNil)
body, _ := io.ReadAll(resp.Body) So(resp.StatusCode, ShouldEqual, http.StatusOK)
healthData := web.HealthData{} body, _ := io.ReadAll(resp.Body)
err = json.Unmarshal(body, &healthData) healthData := web.HealthData{}
assert.Nil(t, err) err = json.Unmarshal(body, &healthData)
assert.Equal(t, "service is ready", healthData.Message) So(err, ShouldBeNil)
assert.NotEmpty(t, healthData.LastChecked) So(healthData.Message, ShouldEqual, readyMessage)
So(healthData.LastChecked, ShouldNotBeZeroValue)
})
} }
func TestReadySSL(t *testing.T) { func TestReadySSL(t *testing.T) {
uri := getServerURL(true, "/health/readyz") Convey("HTTPS ready endpoint returns 200 with ready message", t, func() {
resp, err := http.Get(uri) uri := getServerURL(true, "/health/readyz")
assert.Nil(t, err) resp, err := http.Get(uri)
assert.Equal(t, http.StatusOK, resp.StatusCode) So(err, ShouldBeNil)
body, _ := io.ReadAll(resp.Body) So(resp.StatusCode, ShouldEqual, http.StatusOK)
healthData := web.HealthData{} body, _ := io.ReadAll(resp.Body)
err = json.Unmarshal(body, &healthData) healthData := web.HealthData{}
assert.Nil(t, err) err = json.Unmarshal(body, &healthData)
assert.Equal(t, "service is ready", healthData.Message) So(err, ShouldBeNil)
assert.NotEmpty(t, healthData.LastChecked) So(healthData.Message, ShouldEqual, readyMessage)
So(healthData.LastChecked, ShouldNotBeZeroValue)
})
} }
func TestHealthy(t *testing.T) { func TestHealthy(t *testing.T) {
resp, err := http.Get(getServerURL(false, "/healthz")) Convey("Healthy endpoint returns 200 with running message", t, func() {
assert.Nil(t, err) resp, err := http.Get(getServerURL(false, "/healthz"))
assert.Equal(t, http.StatusOK, resp.StatusCode) So(err, ShouldBeNil)
body, _ := io.ReadAll(resp.Body) So(resp.StatusCode, ShouldEqual, http.StatusOK)
healthData := web.HealthData{} body, _ := io.ReadAll(resp.Body)
err = json.Unmarshal(body, &healthData) healthData := web.HealthData{}
assert.Nil(t, err) err = json.Unmarshal(body, &healthData)
assert.Equal(t, "service up and running", healthData.Message) So(err, ShouldBeNil)
assert.NotEmpty(t, healthData.LastChecked) So(healthData.Message, ShouldEqual, "service up and running")
So(healthData.LastChecked, ShouldNotBeZeroValue)
})
} }
func TestSslEndpoint(t *testing.T) { func TestSslEndpoint(t *testing.T) {
uri := getServerURL(true, "/api/myendpoint") Convey("SSL endpoint returns 200 with expected data", t, func() {
resp, err := http.Get(uri) uri := getServerURL(true, "/api/myendpoint")
assert.Nil(t, err) resp, err := http.Get(uri)
assert.Equal(t, http.StatusOK, resp.StatusCode) So(err, ShouldBeNil)
body, err := ioutil.ReadAll(resp.Body) So(resp.StatusCode, ShouldEqual, http.StatusOK)
assert.Nil(t, err) body, err := io.ReadAll(resp.Body)
t.Logf("Body: %s", string(body)) So(err, ShouldBeNil)
myData := MyData{} myData := MyData{}
err = json.Unmarshal(body, &myData) err = json.Unmarshal(body, &myData)
assert.Nil(t, err) So(err, ShouldBeNil)
assert.NotNil(t, myData) So(myData.Message, ShouldEqual, aMessage)
assert.Equal(t, "A message", myData.Message) So(myData.Count, ShouldBeGreaterThan, 0)
assert.Greater(t, myData.Count, 0) })
} }
+3
View File
@@ -0,0 +1,3 @@
{
"Vulns": null
}
+16 -21
View File
@@ -22,6 +22,10 @@ import (
log "scm.yoorie.de/go-lib/gelf" log "scm.yoorie.de/go-lib/gelf"
) )
const (
errStartingServerLog = "error starting server: %s"
)
type WebServerConfiguration struct { type WebServerConfiguration struct {
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `default:"7080" yaml:"port"` Port int `default:"7080" yaml:"port"`
@@ -202,26 +206,13 @@ func (server *WebServer) Start() error {
server.DebugRoutes("Main", server.router) server.DebugRoutes("Main", server.router)
server.DebugRoutes("Health", server.healthRouter) server.DebugRoutes("Health", server.healthRouter)
if ssl { if ssl {
err := server.setupSsl() if err := server.setupSsl(); err != nil {
if err != nil {
return err return err
} }
} else { } else {
// own http server for the healthchecks if err := server.setupHttp(); err != nil {
httpAddress := server.serviceConfig.BuildHostAddress(false) return err
server.srv = &http.Server{
Addr: httpAddress,
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
Handler: server.router,
} }
go func() {
log.Infof("Starting http server on address: %s", server.srv.Addr)
if err := server.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Alertf("error starting server: %s", err.Error())
}
}()
} }
return nil return nil
} }
@@ -239,7 +230,7 @@ func (server *WebServer) setupHttp() error {
go func() { go func() {
log.Infof("Starting http server on address: %s", server.srv.Addr) log.Infof("Starting http server on address: %s", server.srv.Addr)
if err := server.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := server.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Alertf("error starting server: %s", err.Error()) log.Alertf(errStartingServerLog, err.Error())
} }
}() }()
return nil return nil
@@ -263,7 +254,7 @@ func (server *WebServer) setupSsl() error {
go func() { go func() {
log.Infof("Starting https server on address: %s", server.sslsrv.Addr) log.Infof("Starting https server on address: %s", server.sslsrv.Addr)
if err := server.sslsrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { if err := server.sslsrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
log.Alertf("error starting server: %s", err.Error()) log.Alertf(errStartingServerLog, err.Error())
} }
}() }()
httpAddress := server.serviceConfig.BuildHostAddress(false) httpAddress := server.serviceConfig.BuildHostAddress(false)
@@ -277,7 +268,7 @@ func (server *WebServer) setupSsl() error {
go func() { go func() {
log.Infof("Starting http server on address: %s", server.srv.Addr) log.Infof("Starting http server on address: %s", server.srv.Addr)
if err := server.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := server.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Alertf("error starting server: %s", err.Error()) log.Alertf(errStartingServerLog, err.Error())
} }
}() }()
return nil return nil
@@ -296,9 +287,13 @@ func (server *WebServer) Stop() {
defer cancel() defer cancel()
log.Info("Shutting down server ...") log.Info("Shutting down server ...")
server.srv.Shutdown(ctx) if err := server.srv.Shutdown(ctx); err != nil {
log.Alertf("error shutting down http server: %s", err.Error())
}
if server.isSsl() { if server.isSsl() {
server.sslsrv.Shutdown(ctx) if err := server.sslsrv.Shutdown(ctx); err != nil {
log.Alertf("error shutting down https server: %s", err.Error())
}
} }
log.Info("Server has been shutted down") log.Info("Server has been shutted down")
} }