feat: enhance CI/CD pipeline and add release process documentation

This commit is contained in:
Stefan Goppelt 2026-03-29 20:34:56 +02:00
parent 9f24573dc0
commit 1c44afaae4
9 changed files with 494 additions and 33 deletions

View File

@ -2,11 +2,63 @@ kind: pipeline
type: docker type: docker
name: go-lib/certs name: go-lib/certs
trigger:
event:
- push
- tag
ref:
include:
- refs/heads/**
- refs/tags/v*
steps: steps:
- name: test - name: test
image: golang:1.20.1 image: golang:1.25.8
commands: commands:
- go get ./... - go get ./...
- go test ./... - 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 install golang.org/x/vuln/cmd/govulncheck@latest - go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck -v ./... - 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

46
CHANGELOG.md Normal file
View File

@ -0,0 +1,46 @@
# 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,27 +1,156 @@
package certs package certs
import ( import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"net"
"os"
"os/exec"
"strings"
"testing" "testing"
"time" "time"
"gotest.tools/assert" . "github.com/smartystreets/goconvey/convey"
) )
func TestGenerateTLSConfig(t *testing.T) { const testOrganization = "yoorie.de"
gc := &GenerateCertificate{
Organization: "yoorie.de",
Host: "127.0.0.1",
ValidFor: 10 * 365 * 24 * time.Hour,
IsCA: false,
EcdsaCurve: "P256",
Ed25519Key: true,
}
result, err := gc.GenerateTLSConfig()
assert.Assert(t, err == nil) func parseLeafCertificate(t *testing.T, gc *GenerateCertificate) *x509.Certificate {
assert.Assert(t, result != nil) t.Helper()
assert.Equal(t, 1, len(result.Certificates))
cert := result.Certificates[0] result, err := gc.GenerateTLSConfig()
assert.Assert(t, len(cert.Certificate) > 0) if err != nil {
assert.Assert(t, len(cert.Certificate[0]) > 0) 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,
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)
}
})
} }

View File

@ -1,13 +1,14 @@
<div style="text-align:left"><img src="https://www.yoorie.de/img/favicon_32.png"/></div>
# Go TLS Library # Go TLS Library
[![Build Status](https://drone.yoorie.de/api/badges/go-lib/certs/status.svg)](https://drone.yoorie.de/go-lib/certs) [![Build Status](https://drone.yoorie.de/api/badges/go-lib/certs/status.svg)](https://drone.yoorie.de/go-lib/certs)
## Documentation ## Documentation
Is missed so far and will be created soon. Available project documentation:
- [Changelog](CHANGELOG.md)
- [Definition of Done](docs/DEFINITION_OF_DONE.md)
- [Releasing](docs/RELEASING.md)
--- ---
Copyright &copy; 2023 yoorie.de Copyright &copy; 2023 yoorie.de

122
docs/RELEASING.md Normal file
View File

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

14
go.sum
View File

@ -1,6 +1,8 @@
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 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=

32
scripts/check-coverage.sh Executable file
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"

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/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"