Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5530d0c48 | ||
|
|
b1aaff9f3b | ||
|
|
3e03382781 | ||
|
|
43018ae9ac | ||
|
|
3e384dd8a3 | ||
|
|
821802c0c4 | ||
|
|
2810d93b89 |
@@ -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
|
||||||
@@ -52,11 +52,45 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: go.sum
|
cache-dependency-path: go.sum
|
||||||
|
|
||||||
|
- name: Preflight release API access
|
||||||
|
env:
|
||||||
|
REQUESTED_TAG: ${{ inputs.tag }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -z "${RELEASE_TOKEN:-}" ]]; then
|
||||||
|
echo "No release token available. Set GITEA_TOKEN (or GITHUB_TOKEN on GitHub)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
api_base="${GITHUB_API_URL:-${GITHUB_SERVER_URL%/}/api/v1}"
|
||||||
|
repo_api="${api_base}/repos/${GITHUB_REPOSITORY}"
|
||||||
|
|
||||||
|
curl --fail-with-body -sS \
|
||||||
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${repo_api}" >/dev/null
|
||||||
|
|
||||||
|
curl --fail-with-body -sS \
|
||||||
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${repo_api}/releases?limit=1" >/dev/null
|
||||||
|
|
||||||
|
requested_tag="$(printf '%s' "${REQUESTED_TAG:-}" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')"
|
||||||
|
if [[ -n "$requested_tag" ]]; then
|
||||||
|
normalized_tag="${requested_tag#v}"
|
||||||
|
tag_ref="refs/tags/v${normalized_tag}"
|
||||||
|
if ! git rev-parse --verify --quiet "$tag_ref" >/dev/null; then
|
||||||
|
echo "Requested tag ${tag_ref#refs/tags/} was not found in the checked out repository." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Create or update release
|
- name: Create or update release
|
||||||
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
|
||||||
@@ -177,7 +211,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download released binary
|
- name: Download released binary
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ github.token }}
|
TOKEN: ${{ secrets.GITHUB_TOKEN || secrets.GITEA_TOKEN }}
|
||||||
TAG_NAME: ${{ needs.release.outputs.tag }}
|
TAG_NAME: ${{ needs.release.outputs.tag }}
|
||||||
RELEASE_VERSION: ${{ needs.release.outputs.version }}
|
RELEASE_VERSION: ${{ needs.release.outputs.version }}
|
||||||
ASSET_ARCH: ${{ matrix.asset_arch }}
|
ASSET_ARCH: ${{ matrix.asset_arch }}
|
||||||
|
|||||||
366
.github/copilot-instructions.md
vendored
Normal file
366
.github/copilot-instructions.md
vendored
Normal 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.
|
||||||
70
AGENTS.md
70
AGENTS.md
@@ -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.1`) and keep all vociferate references on the same tag in a workflow.
|
Pin all action references to a released tag (for example `@v1.0.2`) 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.1` (root action)
|
- `git.hrafn.xyz/aether/vociferate@v1.0.2` (root action)
|
||||||
- `git.hrafn.xyz/aether/vociferate/prepare@v1.0.1`
|
- `git.hrafn.xyz/aether/vociferate/prepare@v1.0.2`
|
||||||
- `git.hrafn.xyz/aether/vociferate/publish@v1.0.1`
|
- `git.hrafn.xyz/aether/vociferate/publish@v1.0.2`
|
||||||
- `git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.1`
|
- `git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.2`
|
||||||
|
- `git.hrafn.xyz/aether/vociferate/decorate-pr@v1.0.2`
|
||||||
|
|
||||||
## 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
|
||||||
@@ -28,7 +30,8 @@ Apply these checks before invoking actions:
|
|||||||
|
|
||||||
- Checkout repository first.
|
- Checkout repository first.
|
||||||
- For prepare/publish flows that depend on tags/history, use full history checkout (`fetch-depth: 0`).
|
- For prepare/publish flows that depend on tags/history, use full history checkout (`fetch-depth: 0`).
|
||||||
- Use valid credentials in `github.token` (or explicit token input for `publish` when needed).
|
- Use valid credentials for release/comment API calls. On GitHub, `secrets.GITHUB_TOKEN` is used; on self-hosted Gitea, set `secrets.GITEA_TOKEN`.
|
||||||
|
- `do-release` and `decorate-pr` now run preflight API checks and fail fast when token credentials are missing or insufficient.
|
||||||
- Set required vars/secrets for coverage uploads:
|
- Set required vars/secrets for coverage uploads:
|
||||||
- `vars.ARTEFACT_BUCKET_NAME`
|
- `vars.ARTEFACT_BUCKET_NAME`
|
||||||
- `vars.ARTEFACT_BUCKET_ENDPONT`
|
- `vars.ARTEFACT_BUCKET_ENDPONT`
|
||||||
@@ -91,11 +94,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- id: prepare
|
- id: prepare
|
||||||
uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.1
|
uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.2
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs: prepare
|
needs: prepare
|
||||||
uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.1
|
uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.2
|
||||||
with:
|
with:
|
||||||
tag: ${{ needs.prepare.outputs.version }}
|
tag: ${{ needs.prepare.outputs.version }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
@@ -112,7 +115,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- id: publish
|
- id: publish
|
||||||
uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.1
|
uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.2
|
||||||
with:
|
with:
|
||||||
version: v1.2.3
|
version: v1.2.3
|
||||||
```
|
```
|
||||||
@@ -133,12 +136,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.1
|
uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.2
|
||||||
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.2
|
||||||
|
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.2
|
||||||
|
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 +217,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,3 +242,4 @@ 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.
|
||||||
|
- Do not bypass preflight failures with broad retry loops; fix token scope/secret wiring first.
|
||||||
|
|||||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -19,6 +19,20 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
## [1.0.2] - 2026-03-21
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Documented release/PR-decoration preflight token and API-access checks, including `GITHUB_TOKEN`/`GITEA_TOKEN` behavior for self-hosted Gitea.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
## [1.0.1] - 2026-03-21
|
## [1.0.1] - 2026-03-21
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
@@ -110,7 +124,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]: https://git.hrafn.xyz/aether/vociferate/compare/v1.0.1...main
|
[Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/v1.0.2...main
|
||||||
|
[1.0.2]: https://git.hrafn.xyz/aether/vociferate/compare/v1.0.1...v1.0.2
|
||||||
[1.0.1]: https://git.hrafn.xyz/aether/vociferate/compare/v1.0.0...v1.0.1
|
[1.0.1]: https://git.hrafn.xyz/aether/vociferate/compare/v1.0.0...v1.0.1
|
||||||
[1.0.0]: https://git.hrafn.xyz/aether/vociferate/compare/v0.2.0...v1.0.0
|
[1.0.0]: https://git.hrafn.xyz/aether/vociferate/compare/v0.2.0...v1.0.0
|
||||||
[0.2.0]: https://git.hrafn.xyz/aether/vociferate/compare/v0.1.0...v0.2.0
|
[0.2.0]: https://git.hrafn.xyz/aether/vociferate/compare/v0.1.0...v0.2.0
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -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.1`) instead of `@main`.
|
Release tags now exist; pin all action and reusable-workflow references to the same released tag (for example, `@v1.0.2`) 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.1
|
- uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.2
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version }}
|
version: ${{ inputs.version }}
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs: prepare
|
needs: prepare
|
||||||
uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.1
|
uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.2
|
||||||
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.1
|
- uses: git.hrafn.xyz/aether/vociferate/prepare@v1.0.2
|
||||||
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.1
|
uses: aether/vociferate/.gitea/workflows/do-release.yml@v1.0.2
|
||||||
with:
|
with:
|
||||||
tag: ${{ inputs.tag }}
|
tag: ${{ inputs.tag }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
@@ -95,12 +95,17 @@ Reads the matching section from `CHANGELOG.md` and creates or updates the
|
|||||||
Gitea/GitHub release with those notes. The `version` input is optional — when
|
Gitea/GitHub release with those notes. The `version` input is optional — when
|
||||||
omitted it is derived from the current tag ref automatically.
|
omitted it is derived from the current tag ref automatically.
|
||||||
|
|
||||||
|
The reusable `Do Release` workflow now runs preflight checks before publish to
|
||||||
|
fail fast when the release token is missing or lacks API access. On
|
||||||
|
self-hosted Gitea, set `secrets.GITEA_TOKEN`; on GitHub, `secrets.GITHUB_TOKEN`
|
||||||
|
is used automatically.
|
||||||
|
|
||||||
The `publish` action outputs `release-id` so you can upload additional release
|
The `publish` action outputs `release-id` so you can upload additional release
|
||||||
assets after it runs:
|
assets after it runs:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- id: publish
|
- id: publish
|
||||||
uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.1
|
uses: git.hrafn.xyz/aether/vociferate/publish@v1.0.2
|
||||||
|
|
||||||
- name: Upload my binary
|
- name: Upload my binary
|
||||||
run: |
|
run: |
|
||||||
@@ -120,7 +125,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.1
|
uses: git.hrafn.xyz/aether/vociferate/coverage-badge@v1.0.2
|
||||||
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 +136,33 @@ 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.
|
||||||
|
|
||||||
|
`decorate-pr` also runs a preflight comment API check so workflows fail early
|
||||||
|
with a clear message when token permissions are insufficient.
|
||||||
|
|
||||||
|
```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.2
|
||||||
|
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.2
|
||||||
|
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.
|
||||||
|
|||||||
234
decorate-pr/action.yml
Normal file
234
decorate-pr/action.yml
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
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: Preflight comment API access
|
||||||
|
id: preflight
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
AUTH_TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
|
||||||
|
PR_NUMBER: ${{ steps.validate.outputs.pr_number }}
|
||||||
|
SERVER_URL: ${{ github.server_url }}
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -z "$AUTH_TOKEN" ]]; then
|
||||||
|
echo "No token available for PR comment API calls. Set input token or provide workflow token." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
api_url="${SERVER_URL}/api/v1"
|
||||||
|
if [[ "$SERVER_URL" == *"github.com"* ]]; then
|
||||||
|
api_url="https://api.github.com"
|
||||||
|
fi
|
||||||
|
|
||||||
|
comments_url="${api_url}/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments"
|
||||||
|
|
||||||
|
curl --fail-with-body -sS \
|
||||||
|
-H "Authorization: Bearer ${AUTH_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$comments_url" >/dev/null
|
||||||
|
|
||||||
|
- 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:** $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
|
||||||
@@ -1 +1 @@
|
|||||||
1.0.1
|
1.0.2
|
||||||
|
|||||||
Reference in New Issue
Block a user