chore(ci): add shared-lib scaffolding and functional coverage gate
auth / coverage-baseline (pull_request) Has been skipped
auth / vulnerabilities (pull_request) Successful in 1m46s
auth / test (pull_request) Successful in 2m52s
pre-commit / pre-commit (pull_request) Successful in 5m25s

Bring the auth and logging libs up to the otelsetup/authz_client standard:
golangci-lint + .editorconfig + .testcoverage.yml + cliff.toml + renovate.json
+ CHANGELOG + CLAUDE.md + pre-commit and Release workflows. Replace the
minimal test-only CI with a cache-based coverage-regression gate (PR test job
restores main's baseline from the Actions cache; a non-gating post-merge
coverage-baseline job records it) mirroring the services (ADR-0010 carve-out).
Job names test/vulnerabilities preserved to match branch-protection contexts.
This commit is contained in:
2026-06-15 19:44:14 +02:00
parent 81ac3e6ea5
commit c5e81cc6b5
13 changed files with 351 additions and 1 deletions
+11
View File
@@ -0,0 +1,11 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.go]
indent_style = tab
indent_size = 2
+73 -1
View File
@@ -20,7 +20,79 @@ jobs:
go install mvdan.cc/gofumpt@latest go install mvdan.cc/gofumpt@latest
test -z "$(gofumpt -l .)" test -z "$(gofumpt -l .)"
- name: Run tests - name: Run tests
run: go test -race ./... run: go test -race -coverprofile=coverage.txt ./...
- name: Filter test files from coverage
run: |
grep -v -E '_test\.go:' coverage.txt > coverage.filtered.txt || true
mv coverage.filtered.txt coverage.txt
- name: Check coverage
id: coverage
run: |
go install github.com/vladopajic/go-test-coverage/v2@latest
go-test-coverage --config ./.testcoverage.yml --github-action-output
- name: Restore baseline coverage
uses: actions/cache/restore@v5
with:
path: coverage-baseline.txt
key: coverage-baseline-${{ gitea.run_id }}
restore-keys: |
coverage-baseline-
- name: Compare coverage
run: |
CURRENT="${{ steps.coverage.outputs.total-coverage }}"
if [ -f coverage-baseline.txt ]; then
BASE=$(cat coverage-baseline.txt)
echo "Base coverage: ${BASE}%"
echo "Current coverage: ${CURRENT}%"
if [ "$(echo "$CURRENT < $BASE" | bc -l)" -eq 1 ]; then
echo "::error::Coverage decreased from ${BASE}% to ${CURRENT}%"
exit 1
fi
echo "Coverage maintained or improved: ${BASE}% -> ${CURRENT}%"
else
echo "No baseline coverage found yet, skipping comparison"
echo "Current coverage: ${CURRENT}%"
fi
- name: Post coverage comment
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_URL: ${{ gitea.server_url }}
run: |
COVERAGE="${{ steps.coverage.outputs.total-coverage }}"
curl -X POST "${GITEA_URL}/api/v1/repos/${{ gitea.repository }}/issues/${{ gitea.event.pull_request.number }}/comments" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"body\": \"## Coverage Report\n\nTotal coverage: **${COVERAGE}%**\"}"
coverage-baseline:
# Records main's coverage into the Actions cache for the next PR's
# regression gate to read. Post-merge only, not a required check, blocks
# nothing (cf. ADR-0010).
if: gitea.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: 'stable'
- name: Compute coverage
id: coverage
run: |
go install github.com/vladopajic/go-test-coverage/v2@latest
go test -coverprofile=coverage.txt ./...
grep -v -E '_test\.go:' coverage.txt > coverage.filtered.txt || true
mv coverage.filtered.txt coverage.txt
go-test-coverage --config ./.testcoverage.yml --github-action-output
- name: Write baseline file
run: echo "${{ steps.coverage.outputs.total-coverage }}" > coverage-baseline.txt
- name: Save baseline to cache
uses: actions/cache/save@v5
with:
path: coverage-baseline.txt
key: coverage-baseline-${{ gitea.run_id }}
vulnerabilities: vulnerabilities:
if: gitea.event_name == 'pull_request' if: gitea.event_name == 'pull_request'
runs-on: ubuntu-latest runs-on: ubuntu-latest
+25
View File
@@ -0,0 +1,25 @@
name: pre-commit
permissions: read-all
on:
pull_request:
push:
branches:
- main
jobs:
pre-commit:
runs-on: ubuntu-latest
env:
SKIP: no-commit-to-branch
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: stable
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install goimports
run: go install golang.org/x/tools/cmd/goimports@latest
- uses: pre-commit/action@v3.0.1
+9
View File
@@ -0,0 +1,9 @@
name: Release
on:
push:
branches: [main]
jobs:
release:
uses: unboundsoftware/shared-workflows/.gitea/workflows/Release.yml@main
+1
View File
@@ -2,3 +2,4 @@
.claude .claude
/release /release
coverage.txt coverage.txt
coverage-baseline.txt
+22
View File
@@ -0,0 +1,22 @@
version: "2"
run:
allow-parallel-runners: true
linters:
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+39
View File
@@ -0,0 +1,39 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args:
- --allow-multiple-documents
- id: check-added-large-files
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.25.0
hooks:
- id: commitlint
stages: [ commit-msg ]
additional_dependencies: [ '@commitlint/config-conventional' ]
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: go-mod-tidy
- id: go-imports
args:
- -local
- gitea.unbound.se/shiny/auth
- repo: https://github.com/lietu/go-pre-commit
rev: v1.0.0
hooks:
- id: go-test
- id: gofumpt
- repo: https://github.com/golangci/golangci-lint
rev: v2.12.2
hooks:
- id: golangci-lint-full
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
+13
View File
@@ -0,0 +1,13 @@
# Coverage configuration for go-test-coverage
# https://github.com/vladopajic/go-test-coverage
profile: coverage.txt
threshold:
file: 0
package: 0
total: 0
exclude:
paths:
- _test\.go$
+3
View File
@@ -0,0 +1,3 @@
{
"version": "v0.1.0"
}
+11
View File
@@ -0,0 +1,11 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.1.0] - 2026-06-15
### 🚀 Features
- Initial version: signed `user` header middleware (ADR-0005) and the `MissingDeployedSecrets` startup guard (ADR-0005/0006), extracted from the per-service copies.
<!-- generated by git-cliff -->
+58
View File
@@ -0,0 +1,58 @@
# auth
Shared Go library with authentication primitives for all Shiny backend services.
## Shared Documentation
@../docs/claude/architecture.md
@../docs/claude/go-services.md
@../docs/claude/conventions.md
## Library Information
### Purpose
Single home for the `user`-header auth and secret-startup-guard code that was
previously byte-identical-copied into every backend (the `auth` package and
`cmd/service/secrets_guard.go`). Enforces ADR-0005 (HMAC-signed `user` header,
keyless fail-open only in acctest) and ADR-0006 (fail closed when required
secrets are missing in `staging`/`production`).
### Usage
```go
import "gitea.unbound.se/shiny/auth"
// Fail closed on missing deployed secrets before serving (ADR-0005/0006).
if missing := auth.MissingDeployedSecrets(environment, map[string]string{
"USER_SIGNING_KEY": cfg.UserSigningKey,
"INTERNAL_API_KEY": cfg.InternalAPIKey,
}); len(missing) > 0 {
log.Fatalf("refusing to start: missing secrets in %s: %v", environment, missing)
}
// Verify the gateway's signed user header and inject *User into the context.
handler = auth.UserMiddleware([]byte(cfg.UserSigningKey))(handler)
// Read the authenticated user downstream.
user := auth.FromContext(ctx)
if user.HasRole("admin") { /* ... */ }
```
### Exported API
- `UserMiddleware(signingKey []byte)` — HTTP middleware verifying the
HMAC-signed `user` header; injects `*User` into the request context.
- `FromContext(ctx) *User`, `User.HasRole(...) bool`, `ContextKey`/`UserKey`.
- `MissingDeployedSecrets(environment string, secrets map[string]string) []string`
— returns the sorted names of secrets that are empty in `staging`/`production`
(nil for any other environment, e.g. `development`/acctest).
### Conventions
Standard Shiny library scaffolding: `gofumpt`/`goimports -local`, golangci-lint,
gitleaks and conventional-commit checks via pre-commit; coverage-regression gate
in CI (`.testcoverage.yml`); releases auto-tagged from conventional commits by
the shared Release workflow. Bump the consuming services' `go.mod` after a
release. A breaking change to the signed-header or secret-guard contract is a
cross-service change — see ADR-0005/0006 before changing it.
+80
View File
@@ -0,0 +1,80 @@
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[changelog]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# remove the leading and trailing s
trim = true
# postprocessors
postprocessors = [
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
]
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Replace issue numbers
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}