9 Commits

Author SHA1 Message Date
Micheal Wilkinson
43018ae9ac chore: support both GITHUB_TOKEN and GITEA_TOKEN in do-release workflow
All checks were successful
Push Validation / coverage-badge (push) Successful in 56s
Push Validation / recommend-release (push) Successful in 14s
2026-03-21 12:57:40 +00:00
Micheal Wilkinson
3e384dd8a3 chore: update decorate-pr action version to v1.0.1 in examples 2026-03-21 12:51:50 +00:00
Micheal Wilkinson
821802c0c4 feat: add decorate-pr composite action for pull request review decoration 2026-03-21 12:51:28 +00:00
Micheal Wilkinson
2810d93b89 docs: add copilot instructions for Æther Go workflow 2026-03-21 12:50:27 +00:00
gitea-actions[bot]
02db91114d release: prepare v1.0.1 2026-03-21 11:33:08 +00:00
Micheal Wilkinson
c27b042bb1 fix: restore protocol-relative changelog links
All checks were successful
Push Validation / coverage-badge (push) Successful in 1m44s
Push Validation / recommend-release (push) Successful in 38s
2026-03-21 11:26:31 +00:00
Micheal Wilkinson
59ce683813 docs: remove non-action guardrails from AGENTS.md
Some checks failed
Push Validation / coverage-badge (push) Failing after 42s
Push Validation / recommend-release (push) Has been skipped
2026-03-21 10:03:42 +00:00
Micheal Wilkinson
d653f632d1 fix: add unreleased changelog entry for https:// protocol change
Some checks failed
Push Validation / coverage-badge (push) Failing after 43s
Push Validation / recommend-release (push) Has been skipped
2026-03-21 10:00:44 +00:00
Micheal Wilkinson
8e5d05fce6 fix: replace protocol-relative // URLs with explicit https://
Some checks failed
Push Validation / coverage-badge (push) Failing after 1m59s
Push Validation / recommend-release (push) Has been skipped
2026-03-21 09:57:53 +00:00
10 changed files with 701 additions and 44 deletions

View File

@@ -28,7 +28,7 @@ jobs:
run: run:
shell: bash shell: bash
env: env:
RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN || secrets.GITEA_TOKEN }}
SUMMARY_FILE: ${{ runner.temp }}/do-release-summary.md SUMMARY_FILE: ${{ runner.temp }}/do-release-summary.md
steps: steps:
- name: Checkout tagged revision - name: Checkout tagged revision
@@ -56,7 +56,7 @@ jobs:
id: publish id: publish
uses: ./publish uses: ./publish
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN || secrets.GITEA_TOKEN }}
version: ${{ inputs.tag }} version: ${{ inputs.tag }}
- name: Build release binaries - name: Build release binaries

366
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,366 @@
# Æ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.go` files (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:
```bash
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:
1. Update the changelog (typically `CHANGELOG.md`).
2. Create a separate docs-only commit for changelog updates.
3. 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 guidance`
- `docs: 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.go` files 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:
```go
// 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`, `repository` top-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()` or `fmt.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:
```go
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 `interfaces` packages.
- Consider a `cmd/` directory for application entry points and `internal/` 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.Context` for cancellation and timeouts.
- Protect shared state with mutexes only when channels are not practical.
Example:
```go
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`, or `common` packages; prefer domain-specific package names.
- Keep files focused; one struct per file is reasonable if the struct is substantial.
- Use `context.Context` as 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@v5` with pinned version and go.sum caching).
- Keep workflow summary output using the summary-file pattern:
- Define `SUMMARY_FILE` environment variable per job.
- Append markdown output from steps to the summary file.
- Print the summary in a final `Summary` step with `if: ${{ always() }}` condition.
- 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_PARAMS` is optional; available params are `branch`, `event`, `style` for badge URLs and `branch`, `event` for badge-link targets. Prefer `branch` and `event` when filtering run context; if `style` is 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 in `uses:` references and avoid shorthand owner/repo coordinates.
- Use `prepare` action to update changelog/version and create release tags.
- Use `publish` action 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-badge` action after tests produce `coverage.out` for 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:
```yaml
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 `justfile` for task automation; mirror core CI operations locally.
- **Dependency management**: Use `go.mod` and `go.sum` for 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 `#nosec` comments only with documented justification.
- **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.
- **Dependency hygiene**: Keep `go.mod` and `go.sum` clean; run `go mod tidy` and `go mod verify` regularly.
Integrate both tools into CI workflows; fail builds on high/critical findings.
## Validation Sequence
Execute validation in this order (unless repository policy specifies otherwise):
1. Run focused package tests that directly cover the changed code.
2. Run broader package or module test suites as needed.
3. Run `gosec ./...` for security analysis.
4. Run `govulncheck ./...` for vulnerability scanning.
5. Run full project or behavior/integration suites when change scope or risk warrants it.
6. 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 ./...` and `govulncheck ./...` 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.

View File

@@ -4,14 +4,15 @@ This guide is for agentic coding partners that need to integrate the composite a
## Source Of Truth ## Source Of Truth
Pin all action references to a released tag (for example `@v1.0.0`) and keep all vociferate references on the same tag in a workflow. Pin all action references to a released tag (for example `@v1.0.1`) and keep all vociferate references on the same tag in a workflow.
Published composite actions: Published composite actions:
- `git.hrafn.xyz/aether/vociferate@v1.0.0` (root action) - `git.hrafn.xyz/aether/vociferate@v1.0.1` (root action)
- `git.hrafn.xyz/aether/vociferate/prepare@v1.0.0` - `git.hrafn.xyz/aether/vociferate/prepare@v1.0.1`
- `git.hrafn.xyz/aether/vociferate/publish@v1.0.0` - `git.hrafn.xyz/aether/vociferate/publish@v1.0.1`
- `git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.0` - `git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.1`
- `git.hrafn.xyz/aether/vociferate/decorate-pr@v1.0.1`
## Action Selection Matrix ## Action Selection Matrix
@@ -20,6 +21,7 @@ Use this when deciding which action to call:
- Choose `prepare` when you need to update changelog/version files, commit, and push a release tag. - Choose `prepare` when you need to update changelog/version files, commit, and push a release tag.
- Choose `publish` when a tag already exists and you need to create or update release notes/assets. - Choose `publish` when a tag already exists and you need to create or update release notes/assets.
- Choose `coverage-badge` after tests have produced `coverage.out` and you need coverage artefacts uploaded. - Choose `coverage-badge` after tests have produced `coverage.out` and you need coverage artefacts uploaded.
- Choose `decorate-pr` to annotate pull requests with coverage information and unreleased changelog entries.
- Choose root `vociferate` for direct recommend/prepare logic without commit/tag/push behavior. - Choose root `vociferate` for direct recommend/prepare logic without commit/tag/push behavior.
## Preconditions ## Preconditions
@@ -91,11 +93,11 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- id: prepare - id: prepare
uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.0 uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.1
publish: publish:
needs: prepare needs: prepare
uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.0 uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.1
with: with:
tag: ${{ needs.prepare.outputs.version }} tag: ${{ needs.prepare.outputs.version }}
secrets: inherit secrets: inherit
@@ -112,7 +114,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- id: publish - id: publish
uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.0 uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.1
with: with:
version: v1.2.3 version: v1.2.3
``` ```
@@ -133,12 +135,40 @@ jobs:
- name: Run tests with coverage - name: Run tests with coverage
run: go test -covermode=atomic -coverprofile=coverage.out ./... run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: badge - id: badge
uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.0 uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.1
with: with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }} artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }} artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
``` ```
### 4. Decorate Pull Request With Coverage and Changes
```yaml
jobs:
coverage:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.ARTEFACT_BUCKET_WRITE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.ARTEFACT_BUCKET_WRITE_ACCESS_SECRET }}
AWS_DEFAULT_REGION: ${{ vars.ARTEFACT_BUCKET_REGION }}
AWS_EC2_METADATA_DISABLED: true
steps:
- uses: actions/checkout@v4
- name: Run tests with coverage
run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: badge
uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.1
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
- name: Decorate PR
uses: git.hrafn.xyz/aether/vociferate/decorate-pr@v1.0.1
with:
coverage-percentage: ${{ steps.badge.outputs.total }}
badge-url: ${{ steps.badge.outputs.badge-url }}
```
## Inputs And Outputs Cheatsheet ## Inputs And Outputs Cheatsheet
### prepare ### prepare
@@ -186,6 +216,24 @@ Primary outputs:
- `report-url` - `report-url`
- `badge-url` - `badge-url`
### decorate-pr
Required inputs:
- `coverage-percentage` (0-100, typically from coverage-badge action)
- `badge-url` (SVG badge URL, typically from coverage-badge action)
Useful optional inputs:
- `changelog` (default `CHANGELOG.md`)
- `comment-title` (default `Vociferate Review`)
- `token` (defaults to workflow token)
Primary outputs:
- `comment-id`
- `comment-url`
## Guardrails For Agents ## Guardrails For Agents
Use these rules to avoid common automation mistakes: Use these rules to avoid common automation mistakes:
@@ -193,5 +241,3 @@ Use these rules to avoid common automation mistakes:
- Do not mix action tags in one workflow update. - Do not mix action tags in one workflow update.
- Do not assume a release workflow will run from a tag push in all environments; reusable workflow call paths are supported. - Do not assume a release workflow will run from a tag push in all environments; reusable workflow call paths are supported.
- Do not treat `VOCIFERATE_REPOSITORY_URL` as a full repository URL; it must be a base URL. - Do not treat `VOCIFERATE_REPOSITORY_URL` as a full repository URL; it must be a base URL.
- Keep displayed URLs protocol-relative (`//`) when writing markdown/browser-facing outputs.
- If a workflow environment does not support `GITHUB_STEP_SUMMARY`, append markdown to a file and print it in a final `Summary` step.

View File

@@ -2,8 +2,8 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](//keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](//semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
A `### Breaking` section is used in addition to Keep a Changelog's standard sections to explicitly document changes that are backwards-incompatible but would otherwise appear under `### Changed`. Entries under `### Breaking` trigger a major version bump in automated release recommendation logic. A `### Breaking` section is used in addition to Keep a Changelog's standard sections to explicitly document changes that are backwards-incompatible but would otherwise appear under `### Changed`. Entries under `### Breaking` trigger a major version bump in automated release recommendation logic.
@@ -19,6 +19,20 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
### Fixed ### Fixed
## [1.0.1] - 2026-03-21
### Breaking
### Added
### Changed
### Removed
### Fixed
- Enforced explicit `https://` changelog reference links in prepare output for browser-safe markdown links.
## [1.0.0] - 2026-03-21 ## [1.0.0] - 2026-03-21
### Breaking ### Breaking
@@ -54,7 +68,7 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
### Fixed ### Fixed
- Browser-facing URLs emitted in generated changelog links, workflow summaries, and markdown now use protocol-relative `//` forms. - Browser-facing URLs emitted in generated changelog links, workflow summaries, and markdown now use explicit `https://` forms.
- Release workflows now collect summary markdown into portable temp files and print it in explicit `Summary` steps instead of relying on unsupported `GITHUB_STEP_SUMMARY` output. - Release workflows now collect summary markdown into portable temp files and print it in explicit `Summary` steps instead of relying on unsupported `GITHUB_STEP_SUMMARY` output.
- Prepare now recreates the standard `Unreleased` section headers after promoting notes into a tagged release entry. - Prepare now recreates the standard `Unreleased` section headers after promoting notes into a tagged release entry.
- First-release recommendation remains `v1.0.0` when no prior releases exist in the changelog. - First-release recommendation remains `v1.0.0` when no prior releases exist in the changelog.
@@ -96,7 +110,8 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
- Project/automation rename from `releaseprep` to `vociferate` (entrypoint, package paths, outputs). - Project/automation rename from `releaseprep` to `vociferate` (entrypoint, package paths, outputs).
- README guidance focused on primary cross-repository reuse workflows. - README guidance focused on primary cross-repository reuse workflows.
[Unreleased]: //git.hrafn.xyz/aether/vociferate/compare/v1.0.0...main [Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/v1.0.1...main
[1.0.0]: //git.hrafn.xyz/aether/vociferate/compare/v0.2.0...v1.0.0 [1.0.1]: https://git.hrafn.xyz/aether/vociferate/compare/v1.0.0...v1.0.1
[0.2.0]: //git.hrafn.xyz/aether/vociferate/compare/v0.1.0...v0.2.0 [1.0.0]: https://git.hrafn.xyz/aether/vociferate/compare/v0.2.0...v1.0.0
[0.1.0]: //git.hrafn.xyz/aether/vociferate/compare/2060af6...v0.1.0 [0.2.0]: https://git.hrafn.xyz/aether/vociferate/compare/v0.1.0...v0.2.0
[0.1.0]: https://git.hrafn.xyz/aether/vociferate/compare/2060af6...v0.1.0

View File

@@ -1,9 +1,9 @@
# vociferate # vociferate
[![Main Validation](//git.hrafn.xyz/aether/vociferate/actions/workflows/push-validation.yml/badge.svg?branch=main&event=push)](//git.hrafn.xyz/aether/vociferate/actions/runs/latest?workflow=push-validation.yml&branch=main&event=push) [![Main Validation](https://git.hrafn.xyz/aether/vociferate/actions/workflows/push-validation.yml/badge.svg?branch=main&event=push)](https://git.hrafn.xyz/aether/vociferate/actions/runs/latest?workflow=push-validation.yml&branch=main&event=push)
[![Prepare Release](//git.hrafn.xyz/aether/vociferate/actions/workflows/prepare-release.yml/badge.svg?event=workflow_dispatch)](//git.hrafn.xyz/aether/vociferate/actions/runs/latest?workflow=prepare-release.yml) [![Prepare Release](https://git.hrafn.xyz/aether/vociferate/actions/workflows/prepare-release.yml/badge.svg?event=workflow_dispatch)](https://git.hrafn.xyz/aether/vociferate/actions/runs/latest?workflow=prepare-release.yml)
[![Do Release](//git.hrafn.xyz/aether/vociferate/actions/workflows/do-release.yml/badge.svg?event=push)](//git.hrafn.xyz/aether/vociferate/actions/runs/latest?workflow=do-release.yml) [![Do Release](https://git.hrafn.xyz/aether/vociferate/actions/workflows/do-release.yml/badge.svg?event=push)](https://git.hrafn.xyz/aether/vociferate/actions/runs/latest?workflow=do-release.yml)
[![Coverage](//s3.hrafn.xyz/aether-workflow-report-artefacts/vociferate/branch/main/coverage-badge.svg)](//s3.hrafn.xyz/aether-workflow-report-artefacts/vociferate/branch/main/coverage.html) [![Coverage](https://s3.hrafn.xyz/aether-workflow-report-artefacts/vociferate/branch/main/coverage-badge.svg)](https://s3.hrafn.xyz/aether-workflow-report-artefacts/vociferate/branch/main/coverage.html)
`vociferate` is an `Æther` release orchestration tool written in Go for repositories that `vociferate` is an `Æther` release orchestration tool written in Go for repositories that
want changelog-driven versioning, automated release preparation, and repeatable want changelog-driven versioning, automated release preparation, and repeatable
@@ -16,8 +16,8 @@ revision.
## Use In Other Repositories ## Use In Other Repositories
Vociferate ships three composite actions covering release preparation, release publication, and coverage badge publishing. Vociferate ships composite actions covering release preparation, release publication, coverage badge publishing, and pull request decoration.
Release tags now exist; pin all action and reusable-workflow references to the same released tag (for example, `@v1.0.0`) instead of `@main`. Release tags now exist; pin all action and reusable-workflow references to the same released tag (for example, `@v1.0.1`) instead of `@main`.
For agentic coding partners, see [`AGENTS.md`](AGENTS.md) for a direct integration playbook, selection matrix, and copy-paste workflow patterns. For agentic coding partners, see [`AGENTS.md`](AGENTS.md) for a direct integration playbook, selection matrix, and copy-paste workflow patterns.
@@ -41,13 +41,13 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.0 - uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.1
with: with:
version: ${{ inputs.version }} version: ${{ inputs.version }}
publish: publish:
needs: prepare needs: prepare
uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.0 uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.1
with: with:
tag: ${{ needs.prepare.outputs.version }} tag: ${{ needs.prepare.outputs.version }}
secrets: inherit secrets: inherit
@@ -61,7 +61,7 @@ For repositories that embed the version inside source code, pass `version-file`
and `version-pattern`: and `version-pattern`:
```yaml ```yaml
- uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.0 - uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.1
with: with:
version-file: internal/myapp/version/version.go version-file: internal/myapp/version/version.go
version-pattern: 'const Version = "([^"]+)"' version-pattern: 'const Version = "([^"]+)"'
@@ -85,7 +85,7 @@ on:
jobs: jobs:
release: release:
uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.0 uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.1
with: with:
tag: ${{ inputs.tag }} tag: ${{ inputs.tag }}
secrets: inherit secrets: inherit
@@ -100,7 +100,7 @@ assets after it runs:
```yaml ```yaml
- id: publish - id: publish
uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.0 uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.1
- name: Upload my binary - name: Upload my binary
run: | run: |
@@ -120,7 +120,7 @@ Run your coverage tests first, then call the action to generate `coverage.html`,
run: go test -covermode=atomic -coverprofile=coverage.out ./... run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: coverage - id: coverage
uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.0 uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.1
with: with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }} artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }} artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
@@ -131,6 +131,30 @@ Run your coverage tests first, then call the action to generate `coverage.html`,
echo "Badge: ${{ steps.coverage.outputs.badge-url }}" echo "Badge: ${{ steps.coverage.outputs.badge-url }}"
``` ```
### `decorate-pr` - annotate pull requests with coverage and changes
Decorate pull requests with coverage badges, coverage percentages, and unreleased changelog entries. The action creates a new comment or updates an existing one on each run.
```yaml
- name: Run tests with coverage
run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: coverage
uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.1
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
- name: Decorate pull request
if: github.event_name == 'pull_request'
uses: git.hrafn.xyz/aether/vociferate/decorate-pr@v1.0.1
with:
coverage-percentage: ${{ steps.coverage.outputs.total }}
badge-url: ${{ steps.coverage.outputs.badge-url }}
```
The action automatically finds existing vociferate comments by their marker and updates them instead of creating duplicates. This keeps PR timelines clean while keeping review information current.
## Why The Name ## Why The Name
> **vociferate** _(verb)_: to cry out loudly or forcefully. > **vociferate** _(verb)_: to cry out loudly or forcefully.

View File

@@ -138,8 +138,8 @@ runs:
display_endpoint="${ARTEFACT_BUCKET_ENDPONT#https://}" display_endpoint="${ARTEFACT_BUCKET_ENDPONT#https://}"
display_endpoint="${display_endpoint#http://}" display_endpoint="${display_endpoint#http://}"
report_url="//${display_endpoint%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html" report_url="https://${display_endpoint%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html"
badge_url="//${display_endpoint%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg" badge_url="https://${display_endpoint%/}/${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg"
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp "$COVERAGE_HTML" "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html" --content-type text/html aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp "$COVERAGE_HTML" "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage.html" --content-type text/html
aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp "$COVERAGE_BADGE" "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg" --content-type image/svg+xml aws --endpoint-url "${ARTEFACT_BUCKET_ENDPONT}" s3 cp "$COVERAGE_BADGE" "s3://${ARTEFACT_BUCKET_NAME}/${prefix}/coverage-badge.svg" --content-type image/svg+xml

206
decorate-pr/action.yml Normal file
View File

@@ -0,0 +1,206 @@
name: vociferate/decorate-pr
description: >
Decorate pull requests with coverage badges, unreleased changelog entries,
and other useful review information. Updates existing comment or creates a
new one if it doesn't exist.
inputs:
coverage-percentage:
description: >
Computed coverage percentage (0-100). Typically from coverage-badge
action outputs.
required: true
badge-url:
description: >
Browser-facing URL for the coverage badge image (SVG). Typically from
coverage-badge action outputs.
required: true
changelog:
description: Path to changelog file relative to repository root.
required: false
default: CHANGELOG.md
comment-title:
description: >
Title/identifier for the PR comment. Used to locate existing comment
for updates. Defaults to 'Vociferate Review'.
required: false
default: 'Vociferate Review'
token:
description: >
GitHub or Gitea token for posting PR comments. Defaults to the
workflow token.
required: false
default: ''
outputs:
comment-id:
description: Numeric ID of the posted or updated PR comment.
value: ${{ steps.post-comment.outputs.comment_id }}
comment-url:
description: URL to the posted or updated PR comment.
value: ${{ steps.post-comment.outputs.comment_url }}
runs:
using: composite
steps:
- name: Validate PR context
id: validate
shell: bash
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
echo "This action only works on pull_request events" >&2
exit 1
fi
if [[ -z "$PR_NUMBER" ]]; then
echo "Could not determine PR number from context" >&2
exit 1
fi
printf 'pr_number=%s\n' "$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Extract changelog unreleased entries
id: extract-changelog
shell: bash
env:
CHANGELOG: ${{ inputs.changelog }}
run: |
set -euo pipefail
if [[ ! -f "$CHANGELOG" ]]; then
printf 'unreleased_entries=%s\n' "" >> "$GITHUB_OUTPUT"
exit 0
fi
# Extract everything between [Unreleased] header and the next [X.Y.Z] header
unreleased="$(awk '
/^## \[Unreleased\]/ { in_unreleased=1; next }
/^## \[[0-9]+\.[0-9]+\.[0-9]+\]/ { if (in_unreleased) exit }
in_unreleased && NF { print }
' "$CHANGELOG")"
# Use a temporary file to handle multiline content
tmp_file=$(mktemp)
printf '%s' "$unreleased" > "$tmp_file"
# Read it back and set as output
delimiter="EOF_CHANGELOG"
printf '%s<<%s\n' "unreleased_entries<<$delimiter" "$delimiter" >> "$GITHUB_OUTPUT"
cat "$tmp_file" >> "$GITHUB_OUTPUT"
printf '%s\n' "$delimiter" >> "$GITHUB_OUTPUT"
rm -f "$tmp_file"
- name: Build PR comment markdown
id: build-comment
shell: bash
env:
COMMENT_TITLE: ${{ inputs.comment-title }}
COVERAGE_PCT: ${{ inputs.coverage-percentage }}
BADGE_URL: ${{ inputs.badge-url }}
UNRELEASED: ${{ steps.extract-changelog.outputs.unreleased_entries }}
run: |
set -euo pipefail
# Start building the comment
tmp_file=$(mktemp)
# Add title and coverage section
cat > "$tmp_file" << 'EOF'
<!-- vociferate-pr-review -->
EOF
printf '## %s\n\n' "$COMMENT_TITLE" >> "$tmp_file"
# Coverage badge section
cat >> "$tmp_file" << EOF
### Coverage
![Coverage Badge]($BADGE_URL)
**Coverage:** $COVERAGE_PCT%
EOF
# Changelog section
if [[ -n "$UNRELEASED" ]]; then
cat >> "$tmp_file" << 'EOF'
### Unreleased Changes
EOF
printf '%s\n' "$UNRELEASED" >> "$tmp_file"
printf '\n' >> "$tmp_file"
fi
# Add footer
cat >> "$tmp_file" << 'EOF'
---
*This comment was automatically generated by [vociferate/decorate-pr](https://git.hrafn.xyz/aether/vociferate).*
EOF
# Store as output using delimiter
delimiter="EOF_COMMENT"
printf '%s<<%s\n' "comment_body<<$delimiter" "$delimiter" >> "$GITHUB_OUTPUT"
cat "$tmp_file" >> "$GITHUB_OUTPUT"
printf '%s\n' "$delimiter" >> "$GITHUB_OUTPUT"
rm -f "$tmp_file"
- name: Find and update/post PR comment
id: post-comment
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
PR_NUMBER: ${{ steps.validate.outputs.pr_number }}
COMMENT_BODY: ${{ steps.build-comment.outputs.comment_body }}
SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
API_URL="${SERVER_URL}/api/v1"
if [[ "$SERVER_URL" == *"github.com"* ]]; then
API_URL="https://api.github.com"
fi
# List existing comments to find vociferate comment
comments_url="${API_URL}/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments"
response=$(curl -s -H "Authorization: Bearer ${GITHUB_TOKEN}" "$comments_url")
# Find existing vociferate comment by checking for the marker
existing_comment_id=$(printf '%s' "$response" | \
jq -r '.[] | select(.body | contains("vociferate-pr-review")) | .id' 2>/dev/null | \
head -1 || echo "")
if [[ -n "$existing_comment_id" ]]; then
# Update existing comment
update_url="${API_URL}/repos/${REPOSITORY}/issues/comments/${existing_comment_id}"
curl -s -X PATCH \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(printf '{"body": %s}' "$(printf '%s' "$COMMENT_BODY" | jq -Rs .)")" \
"$update_url" > /dev/null
printf 'comment_id=%s\n' "$existing_comment_id" >> "$GITHUB_OUTPUT"
printf 'comment_url=%s/repos/%s/issues/%s#issuecomment-%s\n' \
"$SERVER_URL" "$REPOSITORY" "$PR_NUMBER" "$existing_comment_id" >> "$GITHUB_OUTPUT"
else
# Create new comment
create_url="${API_URL}/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments"
response=$(curl -s -X POST \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(printf '{"body": %s}' "$(printf '%s' "$COMMENT_BODY" | jq -Rs .)")" \
"$create_url")
comment_id=$(printf '%s' "$response" | jq -r '.id' 2>/dev/null)
printf 'comment_id=%s\n' "$comment_id" >> "$GITHUB_OUTPUT"
printf 'comment_url=%s/repos/%s/issues/%s#issuecomment-%s\n' \
"$SERVER_URL" "$REPOSITORY" "$PR_NUMBER" "$comment_id" >> "$GITHUB_OUTPUT"
fi

View File

@@ -541,10 +541,10 @@ func addChangelogLinks(text, repoURL, rootDir string) string {
func displayURL(url string) string { func displayURL(url string) string {
trimmed := strings.TrimSpace(url) trimmed := strings.TrimSpace(url)
if strings.HasPrefix(trimmed, "https://") { if strings.HasPrefix(trimmed, "https://") {
return "//" + strings.TrimPrefix(trimmed, "https://") return trimmed
} }
if strings.HasPrefix(trimmed, "http://") { if strings.HasPrefix(trimmed, "http://") {
return "//" + strings.TrimPrefix(trimmed, "http://") return "https://" + strings.TrimPrefix(trimmed, "http://")
} }
return trimmed return trimmed
} }

View File

@@ -75,7 +75,7 @@ func (s *PrepareSuite) TestPrepare_UpdatesVersionAndPromotesUnreleasedNotes() {
changelogBytes, err := os.ReadFile(filepath.Join(s.rootDir, "CHANGELOG.md")) changelogBytes, err := os.ReadFile(filepath.Join(s.rootDir, "CHANGELOG.md"))
require.NoError(s.T(), err) require.NoError(s.T(), err)
firstCommit := firstCommitShortHash(s.T(), s.rootDir) firstCommit := firstCommitShortHash(s.T(), s.rootDir)
require.Equal(s.T(), "# Changelog\n\n## [Unreleased]\n\n### Breaking\n\n### Added\n\n### Changed\n\n### Removed\n\n### Fixed\n\n## [1.1.7] - 2026-03-20\n\n### Breaking\n\n### Added\n\n- New thing.\n\n### Fixed\n\n- Old thing.\n\n## [1.1.6] - 2017-12-20\n\n### Fixed\n\n- Historical note.\n\n[Unreleased]: //git.hrafn.xyz/aether/vociferate/compare/v1.1.7...main\n[1.1.7]: //git.hrafn.xyz/aether/vociferate/compare/v1.1.6...v1.1.7\n[1.1.6]: //git.hrafn.xyz/aether/vociferate/compare/"+firstCommit+"...v1.1.6\n", string(changelogBytes)) require.Equal(s.T(), "# Changelog\n\n## [Unreleased]\n\n### Breaking\n\n### Added\n\n### Changed\n\n### Removed\n\n### Fixed\n\n## [1.1.7] - 2026-03-20\n\n### Breaking\n\n### Added\n\n- New thing.\n\n### Fixed\n\n- Old thing.\n\n## [1.1.6] - 2017-12-20\n\n### Fixed\n\n- Historical note.\n\n[Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.7...main\n[1.1.7]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.6...v1.1.7\n[1.1.6]: https://git.hrafn.xyz/aether/vociferate/compare/"+firstCommit+"...v1.1.6\n", string(changelogBytes))
} }
func (s *PrepareSuite) TestPrepare_ReturnsErrorWhenUnreleasedSectionMissing() { func (s *PrepareSuite) TestPrepare_ReturnsErrorWhenUnreleasedSectionMissing() {
@@ -283,9 +283,9 @@ func (s *PrepareSuite) TestPrepare_UsesGitHrafnXYZEnvironmentForChangelogLinks()
require.Contains(s.T(), changelog, "### Removed\n") require.Contains(s.T(), changelog, "### Removed\n")
require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20") require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20")
require.Contains(s.T(), changelog, "## [1.1.6] - 2017-12-20") require.Contains(s.T(), changelog, "## [1.1.6] - 2017-12-20")
require.Contains(s.T(), changelog, "[Unreleased]: //git.hrafn.xyz/aether/vociferate/compare/v1.1.7...main") require.Contains(s.T(), changelog, "[Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.7...main")
require.Contains(s.T(), changelog, "[1.1.7]: //git.hrafn.xyz/aether/vociferate/compare/v1.1.6...v1.1.7") require.Contains(s.T(), changelog, "[1.1.7]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.6...v1.1.7")
require.Contains(s.T(), changelog, "[1.1.6]: //git.hrafn.xyz/aether/vociferate/compare/"+firstCommit+"...v1.1.6") require.Contains(s.T(), changelog, "[1.1.6]: https://git.hrafn.xyz/aether/vociferate/compare/"+firstCommit+"...v1.1.6")
} }
func (s *PrepareSuite) TestPrepare_UsesGitHubEnvironmentForChangelogLinks() { func (s *PrepareSuite) TestPrepare_UsesGitHubEnvironmentForChangelogLinks() {
@@ -305,7 +305,7 @@ func (s *PrepareSuite) TestPrepare_UsesGitHubEnvironmentForChangelogLinks() {
require.Contains(s.T(), changelog, "### Removed\n") require.Contains(s.T(), changelog, "### Removed\n")
require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20") require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20")
require.Contains(s.T(), changelog, "## [1.1.6] - 2017-12-20") require.Contains(s.T(), changelog, "## [1.1.6] - 2017-12-20")
require.Contains(s.T(), changelog, "[Unreleased]: //github.com/aether/vociferate/compare/v1.1.7...main") require.Contains(s.T(), changelog, "[Unreleased]: https://github.com/aether/vociferate/compare/v1.1.7...main")
require.Contains(s.T(), changelog, "[1.1.7]: //github.com/aether/vociferate/compare/v1.1.6...v1.1.7") require.Contains(s.T(), changelog, "[1.1.7]: https://github.com/aether/vociferate/compare/v1.1.6...v1.1.7")
require.Contains(s.T(), changelog, "[1.1.6]: //github.com/aether/vociferate/compare/"+firstCommit+"...v1.1.6") require.Contains(s.T(), changelog, "[1.1.6]: https://github.com/aether/vociferate/compare/"+firstCommit+"...v1.1.6")
} }

View File

@@ -1 +1 @@
1.0.0 1.0.1