Compare commits

..

No commits in common. "51407fe36ff9a81a37e19a1db8b0dbd79d251825" and "95cba2021b343993a886bfb2014a8507a1cd4590" have entirely different histories.

17 changed files with 33 additions and 1104 deletions

View File

@ -2,63 +2,11 @@ kind: pipeline
type: docker
name: go-lib/certs
trigger:
event:
- push
- tag
ref:
include:
- refs/heads/**
- refs/tags/v*
steps:
- name: test
image: golang:1.25.8
image: golang:1.20.1
commands:
- go get ./...
- go vet ./...
- mkdir -p .build
- go test -v -coverprofile .build/coverage.out ./...
- go tool cover -func .build/coverage.out | tee .build/coverage.txt
- bash scripts/check-coverage.sh .build/coverage.out 80
- go test ./...
- 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: package
image: golang:1.25.8
commands:
- tar czf .build/sources.tar.gz --exclude=.build --exclude=.git --exclude=.drone.yml .
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/sources.tar.gz
- .build/release-notes.md
title: ${DRONE_TAG}
note_from_file: .build/release-notes.md
when:
event:
- tag
status:
- success
- govulncheck -v ./...

View File

@ -1,16 +0,0 @@
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
.gitattributes vendored
View File

@ -1,17 +0,0 @@
* 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

View File

@ -1,21 +0,0 @@
# 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`)

View File

@ -1,40 +0,0 @@
#!/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

13
.gitignore vendored
View File

@ -1,13 +0,0 @@
.build/
bin/
dist/
tmp/
coverage/
coverage.out
*.coverprofile
*.test
*.out
.DS_Store
Thumbs.db
.vscode/
.idea/

View File

@ -1,88 +0,0 @@
# 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.

View File

@ -1,46 +0,0 @@
# Changelog
<!-- markdownlint-disable MD024 -->
All notable changes to this project will be documented in this file.
The format follows conventional changelog categories.
## [Unreleased]
## [v0.0.3] - 2026-03-29
### Added
- Added release process documentation in docs/RELEASING.md.
- Added repository changelog file.
- Added GoConvey-based regression tests for certificate generation.
- Added coverage gate script: scripts/check-coverage.sh.
- Added release notes generator script: scripts/generate-release-notes.sh.
- Added tag-based Drone release flow with artifact publishing.
### Changed
- Updated Drone pipeline to run on push and tag events.
- Extended CI quality gates with go vet, coverage reporting, and govulncheck.
- Improved README documentation links and release guidance.
## [v0.0.2] - 2021-03-03
### Added
- Added initial build configuration for the repository.
### Changed
- Updated project README.
## v0.0.1
### Added
- Initial project setup.
[Unreleased]: https://scm.yoorie.de/git/go-lib/certs/compare/v0.0.3...main
[v0.0.3]: https://scm.yoorie.de/git/go-lib/certs/compare/v0.0.2...v0.0.3
[v0.0.2]: https://scm.yoorie.de/git/go-lib/certs/releases/tag/v0.0.2

View File

@ -1,156 +1,27 @@
package certs
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"net"
"os"
"os/exec"
"strings"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"gotest.tools/assert"
)
const testOrganization = "yoorie.de"
func parseLeafCertificate(t *testing.T, gc *GenerateCertificate) *x509.Certificate {
t.Helper()
result, err := gc.GenerateTLSConfig()
if err != nil {
t.Fatalf("GenerateTLSConfig returned error: %v", err)
}
if result == nil || len(result.Certificates) != 1 {
t.Fatalf("expected one certificate, got %#v", result)
}
if len(result.Certificates[0].Certificate) == 0 {
t.Fatal("expected leaf certificate bytes")
}
leaf, err := x509.ParseCertificate(result.Certificates[0].Certificate[0])
if err != nil {
t.Fatalf("ParseCertificate returned error: %v", err)
}
return leaf
}
func TestPublicKey(t *testing.T) {
Convey("publicKey returns the matching public key type", t, func() {
gc := &GenerateCertificate{}
rsaKey, err := rsa.GenerateKey(rand.Reader, 1024)
So(err, ShouldBeNil)
So(gc.publicKey(rsaKey), ShouldResemble, &rsaKey.PublicKey)
ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
So(err, ShouldBeNil)
So(gc.publicKey(ecdsaKey), ShouldResemble, &ecdsaKey.PublicKey)
_, ed25519Key, err := ed25519.GenerateKey(rand.Reader)
So(err, ShouldBeNil)
So(gc.publicKey(ed25519Key), ShouldResemble, ed25519Key.Public().(ed25519.PublicKey))
So(gc.publicKey(struct{}{}), ShouldBeNil)
})
}
func TestGenerateTLSConfig(t *testing.T) {
Convey("GenerateTLSConfig creates valid certificates for supported key types", t, func() {
Convey("RSA certificates include configured SANs and CA settings", func() {
validFrom := "Jan 2 15:04:05 2006"
leaf := parseLeafCertificate(t, &GenerateCertificate{
Organization: testOrganization,
Host: "127.0.0.1,example.com",
ValidFrom: validFrom,
ValidFor: 24 * time.Hour,
IsCA: true,
RSABits: 1024,
})
expectedNotBefore, err := time.Parse("Jan 2 15:04:05 2006", validFrom)
So(err, ShouldBeNil)
So(leaf.PublicKeyAlgorithm, ShouldEqual, x509.RSA)
So(leaf.Subject.Organization, ShouldResemble, []string{testOrganization})
So(leaf.IsCA, ShouldBeTrue)
So(leaf.NotBefore.Equal(expectedNotBefore), ShouldBeTrue)
So(leaf.NotAfter.Sub(leaf.NotBefore), ShouldEqual, 24*time.Hour)
So(leaf.DNSNames, ShouldResemble, []string{"example.com"})
So(len(leaf.IPAddresses), ShouldEqual, 1)
So(leaf.IPAddresses[0].Equal(net.ParseIP("127.0.0.1")), ShouldBeTrue)
})
Convey("Ed25519 certificates are supported when explicitly requested", func() {
leaf := parseLeafCertificate(t, &GenerateCertificate{
Organization: testOrganization,
Host: "localhost",
ValidFor: 12 * time.Hour,
gc := &GenerateCertificate{
Organization: "yoorie.de",
Host: "127.0.0.1",
ValidFor: 10 * 365 * 24 * time.Hour,
IsCA: false,
EcdsaCurve: "P256",
Ed25519Key: true,
})
So(leaf.PublicKeyAlgorithm, ShouldEqual, x509.Ed25519)
So(leaf.IsCA, ShouldBeFalse)
So(leaf.DNSNames, ShouldResemble, []string{"localhost"})
})
Convey("all supported ECDSA curves generate server certificates", func() {
curves := []string{"P224", "P256", "P384", "P521"}
for _, curve := range curves {
leaf := parseLeafCertificate(t, &GenerateCertificate{
Organization: testOrganization,
Host: "certs.example.test",
ValidFor: 6 * time.Hour,
EcdsaCurve: curve,
})
So(leaf.PublicKeyAlgorithm, ShouldEqual, x509.ECDSA)
So(leaf.DNSNames, ShouldResemble, []string{"certs.example.test"})
}
})
})
}
func TestGenerateTLSConfigFatalScenarios(t *testing.T) {
if testCase := os.Getenv("CERTS_FATAL_TEST_CASE"); testCase != "" {
gc := &GenerateCertificate{RSABits: 1024, ValidFor: time.Hour}
switch testCase {
case "invalid-curve":
gc.EcdsaCurve = "invalid"
case "invalid-valid-from":
gc.ValidFrom = "not-a-date"
default:
t.Fatalf("unknown fatal test case: %s", testCase)
}
_, _ = gc.GenerateTLSConfig()
return
}
Convey("GenerateTLSConfig terminates on invalid input", t, func() {
testCases := []struct {
name string
message string
}{
{name: "invalid-curve", message: "Unrecognized elliptic curve"},
{name: "invalid-valid-from", message: "Failed to parse creation date"},
}
for _, testCase := range testCases {
cmd := exec.Command(os.Args[0], "-test.run", "^TestGenerateTLSConfigFatalScenarios$")
cmd.Env = append(os.Environ(), "CERTS_FATAL_TEST_CASE="+testCase.name)
output, err := cmd.CombinedOutput()
So(err, ShouldNotBeNil)
So(strings.Contains(string(output), testCase.message), ShouldBeTrue)
}
})
result, err := gc.GenerateTLSConfig()
assert.Assert(t, err == nil)
assert.Assert(t, result != nil)
assert.Equal(t, 1, len(result.Certificates))
cert := result.Certificates[0]
assert.Assert(t, len(cert.Certificate) > 0)
assert.Assert(t, len(cert.Certificate[0]) > 0)
}

102
README.md
View File

@ -1,103 +1,13 @@
# Go TLS Certificate Helper
<div style="text-align:left"><img src="https://www.yoorie.de/img/favicon_32.png"/></div>
# Go TLS Library
[![Build Status](https://drone.yoorie.de/api/badges/go-lib/certs/status.svg)](https://drone.yoorie.de/go-lib/certs)
Small helper library to generate a self-signed TLS certificate and return it
as a ready-to-use `*tls.Config`.
## Overview
The package builds an in-memory certificate and private key pair from a
`GenerateCertificate` configuration and returns a TLS configuration with one
certificate entry.
Supported key options:
- RSA (default when `EcdsaCurve` is empty and `Ed25519Key` is false)
- Ed25519 (when `Ed25519Key` is true)
- ECDSA curves: `P224`, `P256`, `P384`, `P521`
## Installation
```bash
go get scm.yoorie.de/go-lib/certs
```
## Quick Start
```go
package main
import (
"fmt"
"time"
"scm.yoorie.de/go-lib/certs"
)
func main() {
cfg := &certs.GenerateCertificate{
Organization: "example.org",
Host: "127.0.0.1,localhost,api.example.org",
ValidFor: 365 * 24 * time.Hour,
RSABits: 2048,
}
tlsConfig, err := cfg.GenerateTLSConfig()
if err != nil {
panic(err)
}
fmt.Printf("certificates in config: %d\n", len(tlsConfig.Certificates))
}
```
## API
### Type: `GenerateCertificate`
- `Organization string`: certificate subject organization
- `Host string`: comma-separated DNS names and/or IPs for SAN
- `ValidFrom string`: optional start date in format `Jan 2 15:04:05 2006`
- `ValidFor time.Duration`: certificate validity duration
- `IsCA bool`: whether to mark certificate as CA
- `RSABits int`: RSA key size when RSA is used
- `EcdsaCurve string`: one of `P224`, `P256`, `P384`, `P521`
- `Ed25519Key bool`: generate Ed25519 key when true
### Method
- `GenerateTLSConfig() (*tls.Config, error)`
Creates a self-signed certificate and returns a `*tls.Config` with that
certificate.
## Important Notes
- The certificate is self-signed (issuer equals subject).
- `Host` is split by comma and mapped into DNS or IP SAN entries.
- Invalid `EcdsaCurve` values are not recoverable: the implementation uses
`log.Fatalf`.
- Invalid `ValidFrom` values are not recoverable: the implementation uses
`log.Fatalf`.
## Development
Run quality checks locally:
```bash
go test ./...
go test -coverprofile .build/coverage.out ./...
go tool cover -func .build/coverage.out
go vet ./...
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
```
## Documentation
- [Changelog](CHANGELOG.md)
- [Definition of Done](docs/DEFINITION_OF_DONE.md)
- [Releasing](docs/RELEASING.md)
Is missed so far and will be created soon.
---
Copyright &copy; 2026 yoorie.de
Copyright &copy; 2023 yoorie.de

View File

@ -1,52 +0,0 @@
# 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.

View File

@ -1,122 +0,0 @@
<!-- cspell:ignore Gitea -->
# Releasing go-lib/certs
This document describes the process for creating a release of the
`go-lib/certs` library.
The repository currently has the released tag `v0.0.2`. The next
release created from the current main branch should therefore use the
next patch version, for example `v0.0.3`, unless you intentionally
introduce a breaking or feature-level version bump.
## Overview
Releases in this project are managed via Git tags. When you push a
tag matching the pattern `v*` (for example, `v0.0.3`), 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 CHANGELOG.md docs/
git commit -m "docs: prepare release"
```
### 2. Create tag
```bash
git tag -a v0.0.3 -m "Release v0.0.3"
```
### 3. Push tag
```bash
git push origin v0.0.3
```
### 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/certs
# specific
go get scm.yoorie.de/go-lib/certs@v0.0.3
```
## 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.0.3
git push origin :refs/tags/v0.0.3
```
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

7
go.mod
View File

@ -2,10 +2,9 @@ module scm.yoorie.de/go-lib/certs
go 1.18
require github.com/smartystreets/goconvey v1.8.1
require gotest.tools v2.2.0+incompatible
require (
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
)

14
go.sum
View File

@ -1,8 +1,6 @@
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/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

View File

@ -1,32 +0,0 @@
#!/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"

View File

@ -1,76 +0,0 @@
#!/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/certs/compare/$PREVIOUS_TAG...$CURRENT_TAG"
else
echo "See all changes: https://scm.yoorie.de/git/go-lib/certs/releases/tag/$CURRENT_TAG"
fi
} > "$OUTPUT_FILE"
echo "Release notes generated: $OUTPUT_FILE"
cat "$OUTPUT_FILE"

View File

@ -1,274 +0,0 @@
#!/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