14 KiB
Æther Go Project Workflow Instructions
These instructions apply to all coding work in Æther Go repositories.
This file is self-contained. Do not assume access to this Guides repository or any other Æther repository when following these instructions.
Engineering Process: Strict TDD
Follow TDD (Red, Green, Refactor) for all feature and bug-fix work:
- Red: Write or update a test first. Confirm the test fails for the expected reason.
- Green: Implement the minimum code needed to pass the test.
- Refactor: Improve code only after tests are green.
Do not implement behavior before a failing test exists, unless the user explicitly asks to skip TDD.
Testing Requirements
- Always create and update tests in
*_test.gofiles (Go language standard). - Use the repository's established test framework (prefer testify suites for new coverage where appropriate).
- Run focused tests for touched packages first using
go test -run <TestName> ./path/to/package. - Run broader package or module suites as needed.
- Run full project validation when requested or when change risk warrants it.
- Preserve behavior validated by the repository's behavior/integration suites unless a behavioral change is explicitly requested.
Coverage Standards
Apply coverage gates per package/module, not repository-wide aggregate:
- Target: 80%+ coverage per module.
- Warning zone: 65% to 79.99% (below target, improve during normal engineering work).
- High risk: 50% to 64.99% (requires explicit justification and follow-up).
- Fail gate: Below 50% (unacceptable).
Exclude generated code from coverage calculations. Run coverage analysis for changed modules:
go test -covermode=atomic -coverprofile=coverage.out ./...
Commit Checkpoints
Create commits at these checkpoints unless the user explicitly asks not to commit:
- After writing failing tests (red commit).
- After implementation passes tests (green commit).
- After refactoring (if substantial refactoring occurred).
- After changelog updates (separate docs-only commit).
For each functional change block, create at least one code commit before moving to unrelated work. Keep commits small and scoped to one change unit. Use non-interactive git commands.
Changelog Commits
After completing a change block:
- Update the changelog (typically
CHANGELOG.md). - Create a separate docs-only commit for changelog updates.
- Keep changelog commits scoped to documentation changes only—do not mix code edits into that commit.
Commit Message Guidance
Prefer conventional commits with clear scopes and concise summaries.
- Preferred format for Go maintenance and tooling changes:
chore(go): <summary> - Preferred format for documentation updates:
docs: <summary> - Keep summaries lowercase, imperative, and under 72 characters when possible.
- Use one commit per logical change.
Examples:
chore(go): update dependency injection guidancedocs: clarify security scanning requirements
Go Conventions
- Go version: Target the repository's standard Go toolchain (typically
1.26.1) and maintain compatibility with declared repository settings. - Test files: Keep tests in
*_test.gofiles in the same package as the code being tested. - Test suites: Prefer testify suites for new Go test coverage where appropriate.
- Focused testing: Run package-specific tests first, then broader validation when requested.
- Behavior parity: Use the repository behavior suite for behavior parity validation when relevant.
Dependency Injection (DI) Pattern
Dependency Injection is a required architectural pattern in Æther Go projects. Use the standards below directly:
- Interfaces as contracts: Define interfaces to represent dependencies; place interfaces in the same package as consumers.
- Concrete structs: Implement concrete types as structs that satisfy interfaces.
- Constructor functions: Use
New<TypeName>constructor functions to wire dependencies and validate inputs. - No globals or singletons: Accept dependencies as parameters; avoid hidden global state.
- Testing with DI: Create concrete mock implementations of interfaces for testing; avoid reflection-based mocking.
Example pattern:
// Interface defines a contract.
type UserRepository interface {
GetUser(ctx context.Context, id string) (*User, error)
}
// Concrete implementation satisfies the interface.
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) GetUser(ctx context.Context, id string) (*User, error) {
// Implementation
}
// Constructor function injects dependencies.
func NewPostgresUserRepository(db *sql.DB) (*PostgresUserRepository, error) {
if db == nil {
return nil, errors.New("database connection cannot be nil")
}
return &PostgresUserRepository{db: db}, nil
}
// Consumer type accepts interface dependency.
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) (*UserService, error) {
if repo == nil {
return nil, errors.New("UserRepository cannot be nil")
}
return &UserService{repo: repo}, nil
}
Key rules:
- Validate injected dependencies in constructors; return error if validation fails.
- Keep interfaces minimal and focused on the consumer's contract (Interface Segregation Principle).
- Organize packages by domain, not by layer (avoid
service,handler,repositorytop-level packages). - Break circular dependencies using interfaces; define the interface in the consuming package.
Additional Go Conventions
Beyond dependency injection, follow these Go-specific conventions:
Error Handling
- Return errors as the last return value.
- Use
errors.New()orfmt.Errorf()for simple errors; use custom error types for complex cases. - Wrap errors with context using
fmt.Errorf("%w", err)in Go 1.13+. - Do not log and return errors; let the caller decide how to handle.
Example:
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
if id == "" {
return nil, fmt.Errorf("GetUser: invalid user id")
}
user, err := s.repo.GetUser(ctx, id)
if err != nil {
return nil, fmt.Errorf("GetUser: %w", err)
}
return user, nil
}
Package Organization
- Use domain-driven design principles; packages represent domain entities or use cases.
- Keep package dependencies acyclic; use interfaces to break circular dependencies.
- Place interfaces in consumer packages, not separate
interfacespackages. - Consider a
cmd/directory for application entry points andinternal/for domain logic.
Example structure:
myapp/
cmd/
myapp/
main.go # Dependency wiring
internal/
user/
user.go # Domain model
service.go # UserService and UserRepository interface
repository.go # PostgresUserRepository
auth/
auth.go # Domain model
service.go # AuthService and AuthProvider interface
Concurrency Patterns
- Use goroutines and channels for concurrent work.
- Prefer message passing (channels) over shared memory.
- Use
context.Contextfor cancellation and timeouts. - Protect shared state with mutexes only when channels are not practical.
Example:
func (s *UserService) ProcessUsers(ctx context.Context, ids []string) error {
workers := 5
jobs := make(chan string, len(ids))
errors := make(chan error, len(ids))
for i := 0; i < workers; i++ {
go func() {
for id := range jobs {
if err := s.processUser(ctx, id); err != nil {
errors <- err
}
}
}()
}
for _, id := range ids {
jobs <- id
}
close(jobs)
for i := 0; i < len(ids); i++ {
select {
case err := <-errors:
if err != nil {
return err
}
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
Code Organization and Naming
- Use clear, descriptive names for types and functions.
- Avoid
util,helper, orcommonpackages; prefer domain-specific package names. - Keep files focused; one struct per file is reasonable if the struct is substantial.
- Use
context.Contextas the first parameter in functions that can block or make external calls.
Workflow and Release Standards
When updating CI workflows or release logic:
- Use the repository's standard Go setup (typically
actions/setup-go@v5with pinned version and go.sum caching). - Keep workflow summary output using the summary-file pattern:
- Define
SUMMARY_FILEenvironment variable per job. - Append markdown output from steps to the summary file.
- Print the summary in a final
Summarystep withif: ${{ always() }}condition.
- Define
- Badge URLs must use
https://{HOST}/{OWNER}/{REPO}/actions/workflows/{WORKFLOW_FILE}.yml/badge.svg{?CONTEXT_PARAMS}. - Badge-link targets must use
https://{HOST}/{OWNER}/{REPO}/actions/runs/latest?workflow={WORKFLOW_FILE}.yml{&CONTEXT_PARAMS}. CONTEXT_PARAMSis optional; available params arebranch,event,stylefor badge URLs andbranch,eventfor badge-link targets. Preferbranchandeventwhen filtering run context; ifstyleis used, place it last.- Prefer latest-run pages for badge links for fast status triage.
Composite Actions and Release Orchestration
Use https://git.hrafn.xyz/aether/vociferate as the default release-management tool when integrating Æther composite actions:
- Pin all action references to released tags (for example
@v1.0.0). - Keep all vociferate references on the same tag within a workflow.
- In self-hosted runner environments (git.hrafn.xyz), use explicit
https://action paths inuses:references and avoid shorthand owner/repo coordinates. - Use
prepareaction to update changelog/version and create release tags. - Use
publishaction to create/update release notes and assets from existing tags. - Do not mix alternate release actions unless a repository-local policy explicitly documents an override.
- Use
coverage-badgeaction after tests producecoverage.outfor coverage artifact uploads. - For pre-conditions: checkout with full history (
fetch-depth: 0), valid credentials, and required bucket variables/secrets.
Minimal standalone release workflow example:
name: release
on:
push:
branches: [main]
permissions:
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Vociferate prepare
uses: https://git.hrafn.xyz/aether/vociferate/prepare@v1.0.0
publish:
needs: prepare
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Vociferate publish
uses: https://git.hrafn.xyz/aether/vociferate/publish@v1.0.0
Project Conventions
- Automation: Prefer
justfilefor task automation; mirror core CI operations locally. - Dependency management: Use
go.modandgo.sumfor version tracking. - Structure: Keep code organized in logical packages; avoid deep nesting.
Security Standards
Security standards (self-contained):
-
gosec: Run static security analysis for Go code.
- Command:
gosec ./... - Purpose: Detect common security issues (hard-coded secrets, SQL injection, weak crypto, etc.)
- Suppress: Use
#noseccomments only with documented justification.
- Command:
-
govulncheck: Check Go code and dependencies for known vulnerabilities.
- Command:
govulncheck ./... - Purpose: Detect vulnerabilities in direct and transitive dependencies.
- Address: Update vulnerable dependencies to patched versions.
- Command:
-
Dependency hygiene: Keep
go.modandgo.sumclean; rungo mod tidyandgo mod verifyregularly.
Integrate both tools into CI workflows; fail builds on high/critical findings.
Validation Sequence
Execute validation in this order (unless repository policy specifies otherwise):
- Run focused package tests that directly cover the changed code.
- Run broader package or module test suites as needed.
- Run
gosec ./...for security analysis. - Run
govulncheck ./...for vulnerability scanning. - Run full project or behavior/integration suites when change scope or risk warrants it.
- Verify coverage gates per changed module/class (target 80%, low bound 65%, fail below 50%).
Safety and Scope
- Do not revert unrelated local changes.
- Avoid broad refactors outside the requested scope.
- Keep implementation minimal and aimed only at passing the failing test.
- Do not add code that the test does not exercise.
Disambiguation and Decision-Making
If blocked by ambiguity:
- Ask one concise clarifying question rather than guessing.
- Proceed with the most reasonable interpretation based on context.
- Document any assumption in a commit message or code comment if relevant.
Checklist: Feature or Bug-Fix Completion
Before considering a task done:
- ✓ A failing test existed before implementation (or skip-TDD was explicitly requested).
- ✓ Implementation was minimal and aimed at passing the test only.
- ✓ Refactoring happened only after tests were green.
- ✓ Focused tests passed for all changed packages.
- ✓ Broader validation was run when risk or scope justified it.
- ✓ Coverage gates were evaluated per changed module/class (target 80%, low bound 65%, fail below 50%).
- ✓ Behavioral parity expectations were preserved unless change was explicitly requested.
- ✓ Security scanning passed:
gosec ./...andgovulncheck ./...without unacknowledged findings. - ✓ Dependency injection properly applied: dependencies injected via constructors, not globals; interfaces define contracts; concrete implementations satisfy interfaces.
- ✓ Commits were created at checkpoints (red test, green implementation, changelog).
- ✓ Changelog was updated with a separate docs-only commit.