9 Commits

Author SHA1 Message Date
gitea-actions[bot]
45bb09af27 release: prepare v1.2.0 2026-03-21 23:15:26 +00:00
Micheal Wilkinson
995e397bff docs: update upx fallback note
All checks were successful
Push Validation / coverage-badge (push) Successful in 1m24s
Push Validation / recommend-release (push) Successful in 28s
2026-03-21 23:12:30 +00:00
Micheal Wilkinson
8bf7184479 chore(workflows): install upx via ghaction-upx 2026-03-21 23:12:30 +00:00
Micheal Wilkinson
41918cd5de docs: note containerized upx fallback
All checks were successful
Push Validation / coverage-badge (push) Successful in 1m23s
Push Validation / recommend-release (push) Successful in 25s
2026-03-21 23:02:35 +00:00
Micheal Wilkinson
0cec30c9bb chore(workflows): add container upx fallback 2026-03-21 23:02:29 +00:00
Micheal Wilkinson
24dd65da67 docs: log upx fallback in release workflows
All checks were successful
Push Validation / coverage-badge (push) Successful in 1m18s
Push Validation / recommend-release (push) Successful in 25s
2026-03-21 23:00:08 +00:00
Micheal Wilkinson
1ab56b0536 chore(workflows): allow release builds without upx 2026-03-21 23:00:05 +00:00
Micheal Wilkinson
6919061240 docs: log coverage-gate security hardening
Some checks failed
Push Validation / recommend-release (push) Has been cancelled
Push Validation / coverage-badge (push) Has been cancelled
2026-03-21 22:47:04 +00:00
Micheal Wilkinson
7b739e04c8 chore(go): harden coverage-gate file input handling 2026-03-21 22:47:02 +00:00
8 changed files with 131 additions and 47 deletions

View File

@@ -39,11 +39,10 @@ jobs:
cache: true
cache-dependency-path: go.sum
- name: Install release build tools
run: |
set -euo pipefail
apt-get update
apt-get install -y upx-ucl || apt-get install -y upx
- name: Install UPX
uses: crazy-max/ghaction-upx@v3
with:
install-only: true
- name: Validate formatting
run: test -z "$(gofmt -l .)"
@@ -248,13 +247,13 @@ jobs:
run: |
set -euo pipefail
upx_cmd=""
if command -v upx >/dev/null 2>&1; then
upx_cmd=upx
elif command -v upx-ucl >/dev/null 2>&1; then
upx_cmd=upx-ucl
else
echo "UPX is not available on PATH after installation." >&2
exit 1
echo "UPX is not available on PATH after install step; continuing without binary compression." >&2
fi
mkdir -p dist
@@ -265,7 +264,9 @@ jobs:
bin="vociferate_${RELEASE_VERSION}_${os}_${arch}"
GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags="-s -w" -o "dist/${bin}" ./cmd/vociferate
"${upx_cmd}" --best --lzma "dist/${bin}"
if [[ -n "${upx_cmd}" ]]; then
"${upx_cmd}" --best --lzma "dist/${bin}"
fi
done
(
@@ -345,7 +346,7 @@ jobs:
echo "- Tag: ${TAG_NAME}"
echo "- Release notes sourced from changelog entry ${RELEASE_VERSION}."
echo "- Published assets: vociferate_${RELEASE_VERSION}_linux_amd64, vociferate_${RELEASE_VERSION}_linux_arm64, checksums.txt"
echo "- Release binaries were compressed with UPX before upload."
echo "- Release binaries are compressed with UPX from crazy-max/ghaction-upx@v3 when available, otherwise uploaded uncompressed."
} >> "$SUMMARY_FILE"
else
{

View File

@@ -122,11 +122,10 @@ jobs:
cache: true
cache-dependency-path: go.sum
- name: Install release build tools
run: |
set -euo pipefail
apt-get update
apt-get install -y upx-ucl || apt-get install -y upx
- name: Install UPX
uses: crazy-max/ghaction-upx@v3
with:
install-only: true
- name: Preflight release API access
env:
@@ -170,13 +169,13 @@ jobs:
run: |
set -euo pipefail
upx_cmd=""
if command -v upx >/dev/null 2>&1; then
upx_cmd=upx
elif command -v upx-ucl >/dev/null 2>&1; then
upx_cmd=upx-ucl
else
echo "UPX is not available on PATH after installation." >&2
exit 1
echo "UPX is not available on PATH after install step; continuing without binary compression." >&2
fi
mkdir -p dist
@@ -187,7 +186,9 @@ jobs:
bin="vociferate_${RELEASE_VERSION}_${os}_${arch}"
GOOS="$os" GOARCH="$arch" go build -trimpath -ldflags="-s -w" -o "dist/${bin}" ./cmd/vociferate
"${upx_cmd}" --best --lzma "dist/${bin}"
if [[ -n "${upx_cmd}" ]]; then
"${upx_cmd}" --best --lzma "dist/${bin}"
fi
done
(
@@ -267,7 +268,7 @@ jobs:
echo "- Tag: ${TAG_NAME}"
echo "- Release notes sourced from changelog entry ${RELEASE_VERSION}."
echo "- Published assets: vociferate_${RELEASE_VERSION}_linux_amd64, vociferate_${RELEASE_VERSION}_linux_arm64, checksums.txt"
echo "- Release binaries were compressed with UPX before upload."
echo "- Release binaries are compressed with UPX from crazy-max/ghaction-upx@v3 when available, otherwise uploaded uncompressed."
} >> "$SUMMARY_FILE"
else
{

View File

@@ -4,15 +4,15 @@ This guide is for agentic coding partners that need to integrate the composite a
## Source Of Truth
Pin all action references to a released tag (for example `@v1.1.0`) and keep all vociferate references on the same tag in a workflow.
Pin all action references to a released tag (for example `@v1.2.0`) and keep all vociferate references on the same tag in a workflow.
Published composite actions:
- `https://git.hrafn.xyz/aether/vociferate@v1.1.0` (root action)
- `https://git.hrafn.xyz/aether/vociferate/prepare@v1.1.0`
- `https://git.hrafn.xyz/aether/vociferate/publish@v1.1.0`
- `https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.1.0`
- `https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.1.0`
- `https://git.hrafn.xyz/aether/vociferate@v1.2.0` (root action)
- `https://git.hrafn.xyz/aether/vociferate/prepare@v1.2.0`
- `https://git.hrafn.xyz/aether/vociferate/publish@v1.2.0`
- `https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.2.0`
- `https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.2.0`
## Action Selection Matrix
@@ -88,7 +88,7 @@ Minimal template:
```yaml
jobs:
release:
uses: https://git.hrafn.xyz/aether/vociferate/.gitea/workflows/release.yml@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/.gitea/workflows/release.yml@v1.2.0
with:
version: ${{ inputs.version }}
secrets: inherit
@@ -99,7 +99,7 @@ jobs:
```yaml
jobs:
release:
uses: https://git.hrafn.xyz/aether/vociferate/.gitea/workflows/update-release.yml@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/.gitea/workflows/update-release.yml@v1.2.0
with:
tag: v1.2.3
secrets: inherit
@@ -121,7 +121,7 @@ jobs:
- name: Run tests with coverage
run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: badge
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.2.0
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
@@ -144,12 +144,12 @@ jobs:
- name: Run tests with coverage
run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: badge
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.2.0
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
- name: Decorate PR
uses: https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.2.0
with:
coverage-percentage: ${{ steps.badge.outputs.total }}
badge-url: ${{ steps.badge.outputs.badge-url }}

View File

@@ -13,6 +13,18 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
### Added
### Changed
### Removed
### Fixed
## [1.2.0] - 2026-03-21
### Breaking
### Added
- Extracted `coverage-gate` action and tool from Cue for reuse across Æther projects.
- Coverage gate now available as reusable composite action with JSON metrics output (`passes`, `total_coverage`, `packages_checked`, `packages_failed`).
- Support for per-package coverage threshold policy via JSON configuration in `coverage-gate` tool.
@@ -23,6 +35,9 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
### Fixed
- Hardened `coverage-gate` file input handling by validating and normalizing policy/profile paths before opening files, resolving `G304` findings in `coverage-gate/parse.go`.
- Made release binary builds resilient by installing UPX via `crazy-max/ghaction-upx@v3` and falling back to uncompressed artifacts when UPX is still unavailable in both `release.yml` and `update-release.yml`.
## [1.1.0] - 2026-03-21
### Breaking
@@ -175,10 +190,11 @@ 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).
- README guidance focused on primary cross-repository reuse workflows.
[Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.0...main
[Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/v1.2.0...main
[1.2.0]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.0...v1.2.0
[1.1.0]: https://git.hrafn.xyz/aether/vociferate/compare/v1.0.2...v1.1.0
[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.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.1.0]: https://git.hrafn.xyz/aether/vociferate/compare/81dced6...v0.1.0
[0.1.0]: https://git.hrafn.xyz/aether/vociferate/compare/995e397...v0.1.0

View File

@@ -17,7 +17,7 @@ revision.
## Use In Other Repositories
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.1.0`) instead of `@main`.
Release tags now exist; pin all action and reusable-workflow references to the same released tag (for example, `@v1.2.0`) instead of `@main`.
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:
fetch-depth: 0
- uses: https://git.hrafn.xyz/aether/vociferate/prepare@v1.1.0
- uses: https://git.hrafn.xyz/aether/vociferate/prepare@v1.2.0
with:
version: ${{ inputs.version }}
publish:
needs: prepare
uses: https://git.hrafn.xyz/aether/vociferate/.gitea/workflows/update-release.yml@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/.gitea/workflows/update-release.yml@v1.2.0
with:
tag: ${{ needs.prepare.outputs.version }}
secrets: inherit
@@ -61,7 +61,7 @@ For repositories that embed the version inside source code, pass `version-file`
and `version-pattern`:
```yaml
- uses: https://git.hrafn.xyz/aether/vociferate/prepare@v1.1.0
- uses: https://git.hrafn.xyz/aether/vociferate/prepare@v1.2.0
with:
token: ${{ secrets.RELEASE_PAT }}
version-file: internal/myapp/version/version.go
@@ -86,7 +86,7 @@ on:
jobs:
release:
uses: https://git.hrafn.xyz/aether/vociferate/.gitea/workflows/update-release.yml@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/.gitea/workflows/update-release.yml@v1.2.0
with:
tag: ${{ inputs.tag }}
secrets: inherit
@@ -105,7 +105,7 @@ assets after it runs:
```yaml
- id: publish
uses: https://git.hrafn.xyz/aether/vociferate/publish@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/publish@v1.2.0
- name: Upload my binary
run: |
@@ -125,7 +125,7 @@ Run your coverage tests first, then call the action to generate `coverage.html`,
run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: coverage
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.2.0
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
@@ -150,14 +150,14 @@ with a clear message when token permissions are insufficient.
run: go test -covermode=atomic -coverprofile=coverage.out ./...
- id: coverage
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.2.0
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: https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.2.0
with:
coverage-percentage: ${{ steps.coverage.outputs.total }}
badge-url: ${{ steps.coverage.outputs.badge-url }}
@@ -170,7 +170,7 @@ Enable changelog validation to enforce that code changes include `Unreleased` ch
```yaml
- name: Decorate pull request with changelog gate
if: github.event_name == 'pull_request'
uses: https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.2.0
with:
coverage-percentage: ${{ steps.coverage.outputs.total }}
badge-url: ${{ steps.coverage.outputs.badge-url }}
@@ -196,7 +196,7 @@ Decision outputs enable downstream workflow logic:
- name: Decorate PR and check gate
id: decorate
if: github.event_name == 'pull_request'
uses: https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.1.0
uses: https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.2.0
with:
coverage-percentage: ${{ steps.coverage.outputs.total }}
badge-url: ${{ steps.coverage.outputs.badge-url }}

View File

@@ -43,9 +43,9 @@ type PackageResult struct {
// LoadPolicy reads policy JSON from disk.
func LoadPolicy(path string) (Policy, error) {
f, err := os.Open(path)
f, err := openValidatedReadOnlyFile(path, ".json", "policy")
if err != nil {
return Policy{}, fmt.Errorf("open policy: %w", err)
return Policy{}, err
}
defer f.Close()
@@ -61,9 +61,9 @@ func LoadPolicy(path string) (Policy, error) {
// ParseCoverProfile parses a Go coverprofile and aggregates package coverage.
func ParseCoverProfile(profilePath string, policy Policy) (map[string]Coverage, error) {
f, err := os.Open(profilePath)
f, err := openValidatedReadOnlyFile(profilePath, "", "coverage profile")
if err != nil {
return nil, fmt.Errorf("open coverage profile: %w", err)
return nil, err
}
defer f.Close()
@@ -121,6 +121,41 @@ func ParseCoverProfile(profilePath string, policy Policy) (map[string]Coverage,
return coverage, nil
}
func openValidatedReadOnlyFile(path string, requiredExt string, label string) (*os.File, error) {
cleaned := filepath.Clean(strings.TrimSpace(path))
if cleaned == "" || cleaned == "." {
return nil, fmt.Errorf("invalid %s path", label)
}
if requiredExt != "" {
ext := strings.ToLower(filepath.Ext(cleaned))
if ext != strings.ToLower(requiredExt) {
return nil, fmt.Errorf("invalid %s file extension: got %q, want %q", label, ext, requiredExt)
}
}
absPath, err := filepath.Abs(cleaned)
if err != nil {
return nil, fmt.Errorf("resolve %s path: %w", label, err)
}
info, err := os.Stat(absPath)
if err != nil {
return nil, fmt.Errorf("stat %s: %w", label, err)
}
if info.IsDir() {
return nil, fmt.Errorf("%s path must be a file, got directory", label)
}
// #nosec G304 -- path is explicitly cleaned, normalized, and pre-validated as an existing file.
f, err := os.Open(absPath)
if err != nil {
return nil, fmt.Errorf("open %s: %w", label, err)
}
return f, nil
}
// EvaluateCoverage evaluates package coverage against policy thresholds.
func EvaluateCoverage(packages []string, byPackage map[string]Coverage, policy Policy) []PackageResult {
results := make([]PackageResult, 0, len(packages))

View File

@@ -3,6 +3,7 @@ package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
@@ -86,3 +87,33 @@ func TestEvaluateCoverage_NoStatementsPasses(t *testing.T) {
t.Fatalf("expected pass for no-statement package, got %+v", results[0])
}
}
func TestLoadPolicy_RejectsNonJSONPath(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
policyPath := filepath.Join(tmp, "policy.yaml")
if err := os.WriteFile(policyPath, []byte("minimum_statement_coverage: 80\n"), 0600); err != nil {
t.Fatalf("write policy file: %v", err)
}
_, err := LoadPolicy(policyPath)
if err == nil {
t.Fatal("expected LoadPolicy to fail for non-json extension")
}
if !strings.Contains(err.Error(), "invalid policy file extension") {
t.Fatalf("expected extension error, got: %v", err)
}
}
func TestParseCoverProfile_RejectsDirectoryPath(t *testing.T) {
t.Parallel()
_, err := ParseCoverProfile(t.TempDir(), Policy{MinimumStatementCoverage: 80})
if err == nil {
t.Fatal("expected ParseCoverProfile to fail for directory path")
}
if !strings.Contains(err.Error(), "coverage profile path must be a file") {
t.Fatalf("expected directory path error, got: %v", err)
}
}

View File

@@ -1 +1 @@
1.1.0
1.2.0