feat(shared-skills): add user skill sources
This commit is contained in:
+173
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
# No-excuse rule checker for Go files.
|
||||
# Mirrors the philosophy of python-programmer / typescript-programmer / rust-programmer scripts:
|
||||
# only rules that can be enforced via pure text matching live here.
|
||||
# Everything semantic is on golangci-lint + nilaway + go test -race.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <file.go> [file.go ...]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
violations=0
|
||||
report() {
|
||||
local file="$1"
|
||||
local line="$2"
|
||||
local rule="$3"
|
||||
local detail="$4"
|
||||
echo "::error file=${file},line=${line}::[${rule}] ${detail}" >&2
|
||||
violations=$((violations + 1))
|
||||
}
|
||||
|
||||
is_test_file() {
|
||||
case "$1" in
|
||||
*_test.go) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
is_generated_file() {
|
||||
local file="$1"
|
||||
case "$file" in
|
||||
*.pb.go|*.connect.go|*.gen.go) return 0 ;;
|
||||
*_string.go) return 0 ;;
|
||||
esac
|
||||
# First-line check for "Code generated ... DO NOT EDIT." (the official marker)
|
||||
if [ -f "$file" ]; then
|
||||
head -n 5 "$file" 2>/dev/null | grep -qE "^// Code generated .* DO NOT EDIT\.$" && return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
for file in "$@"; do
|
||||
[ -f "$file" ] || continue
|
||||
case "$file" in
|
||||
*.go) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
if is_generated_file "$file"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
in_test=0
|
||||
if is_test_file "$file"; then
|
||||
in_test=1
|
||||
fi
|
||||
|
||||
line_no=0
|
||||
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
|
||||
line_no=$((line_no + 1))
|
||||
line="$raw_line"
|
||||
|
||||
# Strip line comments before pattern checks
|
||||
# (block comments are not handled — keep the rules robust to that limitation).
|
||||
code_only="${line%%//*}"
|
||||
|
||||
# ── Exemption marker: // no-excuse-ok: <reason> ──────────────────
|
||||
if [[ "$line" =~ //[[:space:]]*no-excuse-ok:[[:space:]]*.+ ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# ── Rule: no `_ = err` (silent error swallow) ────────────────────
|
||||
# The errcheck linter catches most of these but the `_ = err` form
|
||||
# specifically slips through if used with named returns.
|
||||
if [[ "$code_only" =~ ^[[:space:]]*_[[:space:]]*=[[:space:]]*err[[:space:]]*$ ]] ||
|
||||
[[ "$code_only" =~ ^[[:space:]]*_[[:space:]]*=[[:space:]]*err[[:space:]]*[^a-zA-Z0-9_].*$ ]]; then
|
||||
if [ "$in_test" -eq 0 ]; then
|
||||
report "$file" "$line_no" "silent-err" "discarding err with '_ = err' — handle the error"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Rule: no `panic(` in non-test, non-main code ─────────────────
|
||||
# Allowed in main(), allowed in tests, allowed with explicit marker.
|
||||
if [[ "$code_only" =~ [^a-zA-Z0-9_]panic\( ]] || [[ "$code_only" =~ ^[[:space:]]*panic\( ]]; then
|
||||
if [ "$in_test" -eq 0 ]; then
|
||||
# main package main.go is the one exception
|
||||
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
|
||||
if [[ "$pkg_line" != "package main" ]]; then
|
||||
report "$file" "$line_no" "panic-in-lib" "panic outside main/test — return error instead"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Rule: no `log.Fatal` / `log.Panic` in library code ───────────
|
||||
if [[ "$code_only" =~ log\.(Fatal|Panic)(f|ln)?\( ]]; then
|
||||
if [ "$in_test" -eq 0 ]; then
|
||||
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
|
||||
if [[ "$pkg_line" != "package main" ]]; then
|
||||
report "$file" "$line_no" "log-fatal-in-lib" "log.Fatal/Panic outside main — return error"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Rule: no init() functions ─────────────────────────────────
|
||||
# init() ruins testability and creates hidden global state.
|
||||
# Exception: //go:build constraint files and generated code.
|
||||
if [[ "$code_only" =~ ^func[[:space:]]+init\(\)[[:space:]]*\{ ]]; then
|
||||
report "$file" "$line_no" "no-init-func" "init() ruins testability — use explicit constructor"
|
||||
fi
|
||||
|
||||
# ── Rule: no `time.Sleep` in non-test code ──────────────────────
|
||||
if [[ "$code_only" =~ time\.Sleep\( ]]; then
|
||||
if [ "$in_test" -eq 0 ]; then
|
||||
report "$file" "$line_no" "time-sleep" "time.Sleep in production code — use ticker/timer with ctx"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Rule: no `context.Background()` inside functions (only in main/init/test) ──
|
||||
if [[ "$code_only" =~ context\.Background\(\) ]]; then
|
||||
if [ "$in_test" -eq 0 ]; then
|
||||
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
|
||||
if [[ "$pkg_line" != "package main" ]]; then
|
||||
report "$file" "$line_no" "ctx-background-in-lib" "context.Background() outside main — propagate ctx as parameter"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Rule: no `interface{}` (use `any`, the alias from Go 1.18+) ──
|
||||
if [[ "$code_only" =~ interface\{\} ]]; then
|
||||
report "$file" "$line_no" "old-interface-empty" "use 'any' instead of 'interface{}' (Go 1.18+)"
|
||||
fi
|
||||
|
||||
# ── Rule: no bare `fmt.Println` for logging (use slog) ───────────
|
||||
# Acceptable in main.go (CLI output) and tests. Reject in libraries.
|
||||
if [[ "$code_only" =~ fmt\.(Print|Println|Printf)\( ]]; then
|
||||
if [ "$in_test" -eq 0 ]; then
|
||||
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
|
||||
if [[ "$pkg_line" != "package main" ]]; then
|
||||
report "$file" "$line_no" "fmt-print-in-lib" "fmt.Print* in library — use slog for structured logs"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Rule: no `nolint` directive without reason ───────────────────
|
||||
if [[ "$line" =~ //nolint(:|$| ) ]]; then
|
||||
if ! [[ "$line" =~ //nolint:[a-zA-Z0-9_,-]+[[:space:]]+//[[:space:]]*[^[:space:]] ]]; then
|
||||
report "$file" "$line_no" "nolint-no-reason" "//nolint requires a // reason after the linter list"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Rule: no TODO / FIXME without an issue link or owner ─────────
|
||||
# Check the full line — TODOs live in comments, which $code_only has stripped.
|
||||
if echo "$line" | grep -qE '(TODO|FIXME|XXX)([[:space:]]|:)'; then
|
||||
if ! echo "$line" | grep -qE '(TODO|FIXME|XXX).*[(@[]'; then
|
||||
report "$file" "$line_no" "todo-no-owner" "TODO/FIXME requires (#issue) or @owner attribution"
|
||||
fi
|
||||
fi
|
||||
done < "$file"
|
||||
done
|
||||
|
||||
if [ "$violations" -gt 0 ]; then
|
||||
echo "" >&2
|
||||
echo "go-programmer: $violations violation(s). Run also:" >&2
|
||||
echo " gofumpt -l ." >&2
|
||||
echo " golangci-lint run --timeout 5m ./..." >&2
|
||||
echo " nilaway ./..." >&2
|
||||
echo " go test -race -shuffle=on -count=1 ./..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "go-programmer: no-excuse rules passed for $# file(s)."
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "typer",
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
# ─── How to run ───
|
||||
# 1. Install uv (if not installed):
|
||||
# curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
# 2. Run:
|
||||
# uv run new-project.py myservice
|
||||
# uv run new-project.py myservice --module github.com/your-org/myservice
|
||||
# ──────────────────
|
||||
#
|
||||
# Creates a new Go project with the canonical strict layout:
|
||||
# - go.mod with go 1.23
|
||||
# - .golangci.yml (v2, strict bundle)
|
||||
# - Taskfile.yml (fmt + lint + test + build)
|
||||
# - cmd/server/main.go entrypoint
|
||||
# - internal/{cmd,config,api,domain,obs} skeletons
|
||||
# - .github/workflows/ci.yml
|
||||
#
|
||||
# Templates live in ./templates/ — keep this script under 250 pure LOC.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
console = Console(stderr=True)
|
||||
|
||||
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||
|
||||
|
||||
def _render(template_file: str, **subs: str) -> str:
|
||||
"""Read a template file and apply $placeholder substitutions.
|
||||
|
||||
Uses string.Template ($name) so Go/YAML curly braces stay literal.
|
||||
"""
|
||||
raw = (TEMPLATES_DIR / template_file).read_text()
|
||||
if not subs:
|
||||
return raw
|
||||
return Template(raw).substitute(**subs)
|
||||
|
||||
|
||||
# (template-file → relative output path; is_format = .format() is run)
|
||||
FILES: list[tuple[str, str, bool]] = [
|
||||
(".golangci.yml", ".golangci.yml", False),
|
||||
("Taskfile.yml", "Taskfile.yml", False),
|
||||
(".editorconfig", ".editorconfig", False),
|
||||
("gitignore", ".gitignore", False),
|
||||
("ci.yml", ".github/workflows/ci.yml", False),
|
||||
("run.go", "internal/cmd/run.go", False),
|
||||
("config.go", "internal/config/config.go", False),
|
||||
("main.go.tmpl", "cmd/server/main.go", True),
|
||||
("AGENTS.md.tmpl", "AGENTS.md", True),
|
||||
("README.md.tmpl", "README.md", True),
|
||||
]
|
||||
|
||||
|
||||
def _init_go_module(project_dir: Path, module: str) -> None:
|
||||
try:
|
||||
subprocess.run(
|
||||
["go", "mod", "init", module],
|
||||
cwd=project_dir,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
console.print(f" [dim]ran[/] go mod init {module}")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
console.print(f" [yellow]warn[/] go mod init failed ({e}); writing fallback go.mod")
|
||||
(project_dir / "go.mod").write_text(f"module {module}\n\ngo 1.23\n")
|
||||
|
||||
|
||||
def _create_layout(project_dir: Path) -> None:
|
||||
"""Create the canonical internal/ tree."""
|
||||
subdirs = [
|
||||
"cmd/server",
|
||||
"internal/cmd",
|
||||
"internal/config",
|
||||
"internal/api",
|
||||
"internal/domain",
|
||||
"internal/obs",
|
||||
".github/workflows",
|
||||
]
|
||||
for sd in subdirs:
|
||||
(project_dir / sd).mkdir(parents=True)
|
||||
|
||||
|
||||
def _write_files(project_dir: Path, name: str, module: str, purpose: str) -> None:
|
||||
"""Render every template into the project tree."""
|
||||
for tmpl_name, out_rel, is_format in FILES:
|
||||
subs = (
|
||||
{"name": name, "module": module, "short_purpose": purpose}
|
||||
if is_format
|
||||
else {}
|
||||
)
|
||||
content = _render(tmpl_name, **subs)
|
||||
out_path = project_dir / out_rel
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(content)
|
||||
console.print(f" [dim]wrote[/] {out_rel}")
|
||||
|
||||
|
||||
def main(
|
||||
name: str,
|
||||
path: str = typer.Option(".", help="Parent dir"),
|
||||
module: str = typer.Option("", help="Go module path; default: <name>"),
|
||||
purpose: str = typer.Option("HTTP", help="Short purpose for AGENTS.md"),
|
||||
) -> None:
|
||||
"""Scaffold a new Go project with the strict toolchain."""
|
||||
project_dir = Path(path) / name
|
||||
if project_dir.exists():
|
||||
console.print(f"[red]✗[/red] {project_dir} already exists")
|
||||
sys.exit(1)
|
||||
|
||||
module_path = module or name
|
||||
|
||||
project_dir.mkdir(parents=True)
|
||||
_create_layout(project_dir)
|
||||
_init_go_module(project_dir, module_path)
|
||||
_write_files(project_dir, name, module_path, purpose)
|
||||
|
||||
console.print(f"\n[bold green]Done![/] cd {project_dir}")
|
||||
console.print(" go get github.com/caarlos0/env/v11")
|
||||
console.print(" task # fmt + lint + test")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
typer.run(main)
|
||||
@@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{yml,yaml,json,md}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,95 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
tests: true
|
||||
modules-download-mode: readonly
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- govet
|
||||
- staticcheck
|
||||
- errcheck
|
||||
- errorlint
|
||||
- nilerr
|
||||
- nilnil
|
||||
- bodyclose
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- contextcheck
|
||||
- fatcontext
|
||||
- copyloopvar
|
||||
- intrange
|
||||
- usetesting
|
||||
- testifylint
|
||||
- gofumpt
|
||||
- goimports
|
||||
- whitespace
|
||||
- misspell
|
||||
- unconvert
|
||||
- unparam
|
||||
- ineffassign
|
||||
- dupword
|
||||
- gocognit
|
||||
- gocyclo
|
||||
- funlen
|
||||
- lll
|
||||
- nestif
|
||||
- dupl
|
||||
- revive
|
||||
- unused
|
||||
- exhaustive
|
||||
- gosec
|
||||
- sloglint
|
||||
- perfsprint
|
||||
- prealloc
|
||||
- makezero
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
check-blank: true
|
||||
errorlint:
|
||||
errorf: true
|
||||
asserts: true
|
||||
comparison: true
|
||||
gocognit:
|
||||
min-complexity: 25
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
funlen:
|
||||
lines: 90
|
||||
statements: 60
|
||||
lll:
|
||||
line-length: 120
|
||||
tab-width: 4
|
||||
nestif:
|
||||
min-complexity: 4
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: false
|
||||
check: [switch, map]
|
||||
sloglint:
|
||||
no-mixed-args: true
|
||||
attr-only: true
|
||||
no-global: all
|
||||
context: scope
|
||||
static-msg: true
|
||||
no-raw-keys: true
|
||||
key-naming-case: snake
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofumpt
|
||||
- goimports
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
exclude-rules:
|
||||
- path: _test\\.go
|
||||
linters: [funlen, lll, dupl, gosec]
|
||||
- path: \\.pb\\.go$
|
||||
linters: [all]
|
||||
- path: \\.connect\\.go$
|
||||
linters: [all]
|
||||
@@ -0,0 +1,24 @@
|
||||
# AGENTS.md
|
||||
|
||||
Go 1.23+ $short_purpose service.
|
||||
|
||||
## Commands
|
||||
- `task` — fmt + lint + test
|
||||
- `task build` — produce ./bin/server
|
||||
- `task ci` — full CI pipeline locally
|
||||
|
||||
## Architecture
|
||||
- `cmd/server/main.go` — entrypoint, ≤50 LOC
|
||||
- `internal/cmd/` — root command, signal wiring
|
||||
- `internal/api/` — HTTP handlers + middleware (gin)
|
||||
- `internal/domain/` — smart-constructor types, no I/O
|
||||
- `internal/store/` — DB layer (sqlc-generated, never hand-edited)
|
||||
- `internal/config/` — env-driven Config
|
||||
- `internal/obs/` — slog setup, observability
|
||||
|
||||
## Conventions
|
||||
- `slog` for all logs; never `log.*`, never `fmt.Println` in libs
|
||||
- `context.Context` first arg for every public function with I/O
|
||||
- Errors wrapped with `%w`; check with `errors.Is/As`
|
||||
- 250 pure LOC ceiling per file
|
||||
- Tests follow Given/When/Then; less mock the better
|
||||
@@ -0,0 +1,12 @@
|
||||
# $name
|
||||
|
||||
Bootstrapped with the `programming` skill's Go scaffold.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
task # fmt + lint + test
|
||||
task run # build + run server
|
||||
```
|
||||
|
||||
See `AGENTS.md` for architecture conventions.
|
||||
@@ -0,0 +1,40 @@
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
BINARY: server
|
||||
PKG: ./cmd/server
|
||||
|
||||
tasks:
|
||||
default:
|
||||
deps: [fmt, lint, test]
|
||||
|
||||
fmt:
|
||||
cmds:
|
||||
- gofumpt -w .
|
||||
- goimports -w -local "$(go list -m)" .
|
||||
|
||||
lint:
|
||||
cmds:
|
||||
- golangci-lint run --timeout 5m ./...
|
||||
- nilaway ./... || true
|
||||
|
||||
test:
|
||||
cmds:
|
||||
- go test -race -shuffle=on -count=1 ./...
|
||||
|
||||
test-cover:
|
||||
cmds:
|
||||
- go test -race -shuffle=on -count=1 -coverprofile=coverage.out ./...
|
||||
- go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
build:
|
||||
cmds:
|
||||
- go build -trimpath -ldflags="-s -w" -o bin/{{.BINARY}} {{.PKG}}
|
||||
|
||||
run:
|
||||
deps: [build]
|
||||
cmds:
|
||||
- ./bin/{{.BINARY}}
|
||||
|
||||
ci:
|
||||
deps: [fmt, lint, test, build]
|
||||
@@ -0,0 +1,37 @@
|
||||
name: ci
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
cache: true
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
go install mvdan.cc/gofumpt@latest
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.0.0
|
||||
go install go.uber.org/nilaway/cmd/nilaway@latest
|
||||
go install github.com/go-task/task/v3/cmd/task@latest
|
||||
|
||||
- name: Format check
|
||||
run: gofumpt -l . | (! grep .)
|
||||
|
||||
- name: Lint
|
||||
run: golangci-lint run --timeout 5m ./...
|
||||
|
||||
- name: Nilaway
|
||||
run: nilaway ./... || true
|
||||
|
||||
- name: Test
|
||||
run: go test -race -shuffle=on -count=1 ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -trimpath ./...
|
||||
@@ -0,0 +1,24 @@
|
||||
// Package config loads typed config from env.
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host string `env:"HOST" envDefault:"0.0.0.0"`
|
||||
Port int `env:"PORT" envDefault:"8080"`
|
||||
ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" envDefault:"20s"`
|
||||
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
|
||||
LogFormat string `env:"LOG_FORMAT" envDefault:"json"`
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
var cfg Config
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
bin/
|
||||
coverage.out
|
||||
coverage.html
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
.env
|
||||
.env.local
|
||||
|
||||
*.pem
|
||||
*.key
|
||||
@@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"$module/internal/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
if err := cmd.Execute(ctx); err != nil {
|
||||
slog.Error("fatal", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Package cmd wires the root command and subcommands.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Execute runs the root command. Wire cobra/subcommands here.
|
||||
func Execute(ctx context.Context) error {
|
||||
slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
slog.InfoContext(ctx, "starting")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user