25 Commits

Author SHA1 Message Date
Micheal Wilkinson
ac41276c50 ci: Correct pipeline
Some checks failed
Release / prepare (push) Failing after 7s
Push Validation / validate (push) Failing after 13m8s
2026-03-21 23:41:13 +00:00
Micheal Wilkinson
b97da893fb docs: update changelog for local release automation
All checks were successful
Push Validation / validate (push) Successful in 2m29s
2026-03-21 23:21:40 +00:00
Micheal Wilkinson
b24ca1214c ci(release): replace vociferate with local release scripts 2026-03-21 23:21:40 +00:00
Micheal Wilkinson
76460cddee docs: update changelog for runner compatibility
Some checks failed
Release / prepare (push) Failing after 5s
Release / publish (push) Has been skipped
Push Validation / validate (push) Successful in 3m23s
2026-03-21 23:15:32 +00:00
Micheal Wilkinson
d63a8bb615 ci: remove fragile external badge actions 2026-03-21 23:15:32 +00:00
ced23e0156 Update README.md
Some checks failed
Release / prepare (push) Failing after 4s
Release / publish (push) Has been skipped
Push Validation / check-open-pr (push) Successful in 3s
Push Validation / validate (push) Failing after 20s
2026-03-21 23:08:31 +00:00
Micheal Wilkinson
710fe049f5 docs: update changelog for pr validation fallbacks
Some checks failed
Pull Request Validation / validate (pull_request) Successful in 4m34s
Release / prepare (push) Failing after 4s
Push Validation / check-open-pr (push) Successful in 3s
Release / publish (push) Has been skipped
Push Validation / validate (push) Failing after 16s
2026-03-21 23:02:46 +00:00
Micheal Wilkinson
2294bb940b ci(pr-validation): harden decoration and summary fallback 2026-03-21 23:02:46 +00:00
Micheal Wilkinson
bbbacb0eb6 docs: update changelog for workflow hardening
Some checks failed
Push Validation / check-open-pr (push) Successful in 2s
Push Validation / validate (push) Has been skipped
Pull Request Validation / validate (pull_request) Failing after 2m9s
2026-03-21 22:54:07 +00:00
Micheal Wilkinson
28820748f7 ci: harden workflow dedup and badge gating 2026-03-21 22:54:07 +00:00
Micheal Wilkinson
1f93a3d532 docs: update changelog for push dedup guard
Some checks failed
Push Validation / check-open-pr (push) Failing after 2s
Push Validation / validate (push) Has been skipped
Pull Request Validation / validate (pull_request) Failing after 1m44s
2026-03-21 22:36:23 +00:00
Micheal Wilkinson
3104feb738 ci(push-validation): skip branch pushes with open PR 2026-03-21 22:36:23 +00:00
Micheal Wilkinson
e1a58b6607 docs: update changelog for concurrency deduplication
Some checks failed
Push Validation / validate (push) Has been cancelled
Pull Request Validation / validate (pull_request) Failing after 2m8s
2026-03-21 22:32:35 +00:00
Micheal Wilkinson
411c99532d ci: deduplicate runs via shared branch-name concurrency group 2026-03-21 21:21:33 +00:00
Micheal Wilkinson
607f43eaa0 docs: update changelog for push-validation branch trigger
Some checks failed
Push Validation / validate (push) Has been cancelled
Pull Request Validation / validate (pull_request) Failing after 1m54s
2026-03-21 21:18:56 +00:00
Micheal Wilkinson
0691c54965 ci(push-validation): trigger on all branches 2026-03-21 21:17:56 +00:00
Micheal Wilkinson
74640ddaa8 docs: update changelog for duplicate-run prevention
Some checks failed
Pull Request Validation / validate (pull_request) Failing after 2m0s
2026-03-21 21:15:19 +00:00
Micheal Wilkinson
354f3599b4 ci(push-validation): trigger only on main pushes 2026-03-21 21:15:19 +00:00
Micheal Wilkinson
ae86431d50 docs: update changelog for PR decoration gate fallback
Some checks failed
Pull Request Validation / validate (pull_request) Failing after 2m29s
Push Validation / validate (push) Successful in 3m7s
2026-03-21 21:12:10 +00:00
Micheal Wilkinson
9c7f6fbdf4 ci(pr-validation): fallback changelog gate and fix badge condition 2026-03-21 21:12:09 +00:00
Micheal Wilkinson
cf183d9bb0 docs: update changelog for badge upload guard
Some checks failed
Pull Request Validation / validate (pull_request) Failing after 2m32s
Push Validation / validate (push) Successful in 3m3s
2026-03-21 21:07:43 +00:00
Micheal Wilkinson
65d0a95968 ci(pr-validation): guard badge upload on coverage file 2026-03-21 21:07:43 +00:00
Micheal Wilkinson
7fbbb442a0 ci(pr-validation): always run badge upload and PR decoration
Some checks failed
Push Validation / validate (push) Successful in 4m41s
Pull Request Validation / validate (pull_request) Failing after 3m30s
2026-03-21 20:59:39 +00:00
Micheal Wilkinson
a316723cfc docs: update changelog for gosec scanner fix
Some checks failed
Push Validation / validate (push) Has been cancelled
Pull Request Validation / validate (pull_request) Failing after 4m5s
2026-03-21 20:58:17 +00:00
Micheal Wilkinson
7405044fb5 chore(go): annotate intentional command execution for gosec 2026-03-21 20:58:17 +00:00
9 changed files with 331 additions and 45 deletions

View File

@@ -7,6 +7,10 @@ on:
- synchronize
- reopened
concurrency:
group: ci-${{ github.head_ref }}
cancel-in-progress: true
jobs:
validate:
runs-on: ubuntu-latest
@@ -149,32 +153,91 @@ jobs:
cache: true
cache-dependency-path: go.sum
- name: Check coverage artefacts
id: coverage-files
if: ${{ always() && steps.coverage.outcome == 'success' }}
run: |
set -euo pipefail
if [[ -f coverage.out ]]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "coverage.out was not produced; skipping coverage badge upload." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload coverage badge
id: badge
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.1.0
with:
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
if: ${{ always() && steps.coverage.outcome == 'success' && steps.coverage-files.outputs.exists == 'true' }}
run: |
set -euo pipefail
total="$(go tool cover -func=coverage.out | awk '/^total:/ {sub(/%/, "", $3); print $3}')"
if [[ -z "$total" ]]; then
total="n/a"
fi
echo "total=${total}" >> "$GITHUB_OUTPUT"
echo "report-url=n/a" >> "$GITHUB_OUTPUT"
echo "badge-url=n/a" >> "$GITHUB_OUTPUT"
- name: Validate changelog gate
if: ${{ always() }}
run: |
set -euo pipefail
if ! awk '
/^## \[Unreleased\]/ { in_unreleased=1; next }
/^## \[/ && in_unreleased { exit 0 }
in_unreleased && /^- / { found=1 }
END { exit found ? 0 : 1 }
' CHANGELOG.md; then
echo "Missing changelog entry under [Unreleased]." >&2
exit 1
fi
- name: Decorate PR
if: ${{ always() && github.server_url == 'https://github.com' && steps.badge.outcome == 'success' }}
uses: https://git.hrafn.xyz/aether/vociferate/decorate-pr@v1.1.0
continue-on-error: true
with:
coverage-percentage: ${{ steps.badge.outputs.total }}
badge-url: ${{ steps.badge.outputs.badge-url }}
enable-changelog-gate: 'true'
changelog-gate-mode: strict
enable-changelog-gate: 'false'
- name: Skip external PR decoration on non-GitHub runners
if: ${{ always() && github.server_url != 'https://github.com' }}
run: |
set -euo pipefail
echo "Skipping decorate-pr action on ${GITHUB_SERVER_URL}; external composite action is not stable on this runner." >> "$GITHUB_STEP_SUMMARY"
- name: Add coverage summary
if: ${{ always() }}
run: |
set -euo pipefail
total="${{ steps.badge.outputs.total }}"
report_url="${{ steps.badge.outputs.report-url }}"
badge_url="${{ steps.badge.outputs.badge-url }}"
if [[ -z "$total" ]]; then
total="n/a"
fi
if [[ -z "$report_url" ]]; then
report_url="n/a"
fi
if [[ -z "$badge_url" ]]; then
badge_url="n/a"
fi
{
echo '## Coverage'
echo
echo '- Total: `${{ steps.badge.outputs.total }}%`'
echo '- Report: ${{ steps.badge.outputs.report-url }}'
echo '- Badge: ${{ steps.badge.outputs.badge-url }}'
echo "- Total: ${total}%"
echo "- Report: ${report_url}"
echo "- Badge: ${badge_url}"
echo
echo '### Package Coverage'
cat coverage-packages.md
if [[ -f coverage-packages.md ]]; then
cat coverage-packages.md
else
echo '_Package coverage details unavailable for this run._'
fi
} >> "$SUMMARY_FILE"
- name: Run behavior suite

View File

@@ -9,6 +9,7 @@ permissions:
jobs:
prepare:
if: "${{ !startsWith(github.event.head_commit.message, 'chore(release): prepare ') }}"
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -23,24 +24,15 @@ jobs:
ln -s CHANGELOG.md changelog.md
fi
- name: Vociferate prepare
uses: https://git.hrafn.xyz/aether/vociferate/prepare@v1.1.0
- name: Prepare release
run: bash ./script/prepare-release.sh
publish:
needs: prepare
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Provide lowercase changelog compatibility
- name: Summary
if: ${{ always() }}
run: |
set -euo pipefail
if [[ -f CHANGELOG.md && ! -e changelog.md ]]; then
ln -s CHANGELOG.md changelog.md
fi
- name: Vociferate publish
uses: https://git.hrafn.xyz/aether/vociferate/publish@v1.1.0
if git rev-parse -q --verify "refs/tags/$(sed -n 's/^const String = "\([^"]*\)"$/v\1/p' internal/homesick/version/version.go)" >/dev/null; then
echo "Prepared and pushed release tag $(sed -n 's/^const String = "\([^"]*\)"$/v\1/p' internal/homesick/version/version.go)." >> "$GITHUB_STEP_SUMMARY"
else
echo "No release prepared in this run." >> "$GITHUB_STEP_SUMMARY"
fi

View File

@@ -7,6 +7,10 @@ on:
tags-ignore:
- "*"
concurrency:
group: ci-${{ github.ref_name }}
cancel-in-progress: true
jobs:
validate:
runs-on: ubuntu-latest
@@ -158,19 +162,27 @@ jobs:
exit 1
fi
- name: Publish coverage artefacts
id: coverage-badge
uses: https://git.hrafn.xyz/aether/vociferate/coverage-badge@v1.1.0
with:
coverage-profile: coverage.out
coverage-html: coverage.html
coverage-badge: coverage-badge.svg
coverage-summary: coverage-summary.json
artefact-bucket-name: ${{ vars.ARTEFACT_BUCKET_NAME }}
artefact-bucket-endpoint: ${{ vars.ARTEFACT_BUCKET_ENDPONT }}
branch-name: ${{ github.ref_name }}
repository-name: ${{ github.repository }}
summary-file: ${{ env.SUMMARY_FILE }}
- name: Add coverage summary
if: ${{ always() && steps.coverage-tests.outcome == 'success' }}
run: |
set -euo pipefail
total="${{ steps.coverage-tests.outputs.total }}"
if [[ -z "$total" ]]; then
total="n/a"
fi
{
echo '## Coverage'
echo
echo "- Total: ${total}%"
echo
echo '### Package Coverage'
if [[ -f coverage-packages.md ]]; then
cat coverage-packages.md
else
echo '_Package coverage details unavailable for this run._'
fi
} >> "$SUMMARY_FILE"
- name: Run behavior suite on main pushes
if: ${{ github.ref == 'refs/heads/main' }}

View File

@@ -89,5 +89,13 @@ jobs:
ln -s CHANGELOG.md changelog.md
fi
- name: Vociferate publish
uses: https://git.hrafn.xyz/aether/vociferate/publish@v1.1.0
- name: Install jq
run: |
set -euo pipefail
apt-get update
apt-get install -y jq
- name: Create or update release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash ./script/publish-release.sh

View File

@@ -28,6 +28,19 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
- `golang/govulncheck-action` in push and PR validation now passes explicit `go-package`, cache enablement, and `cache-dependency-path` inputs to match the required workflow pattern.
- CLI/core wiring now injects `stdin` through `core.NewApp`, `main` owns the `GIT_TERMINAL_PROMPT=0` side effect, and `Rc` force handling is passed per call instead of mutating shared app state.
- Core filesystem and git error paths now wrap underlying failures with command-specific context across listing, generation, tracking, linking, rc hook execution, and destroy confirmation flows.
- Gosec compliance updated for intentional command execution paths: `Open()` now documents both `G702` and `G204` suppression rationale, and fixed-`git` helper invocations include explicit `G204` justifications.
- PR validation badge upload now runs only when `coverage.out` exists, preventing downstream badge artefact failures while still allowing PR decoration to run on failed jobs.
- PR validation now keys coverage badge upload off the coverage step outcome and performs changelog gate validation in a native workflow step; decorate-pr changelog gating is disabled to bypass the broken internal extractor action.
- Push validation now triggers on all branches, not only `main`.
- Push and PR validation workflows now share a `concurrency` group keyed on the branch name (`github.ref_name` / `github.head_ref`) with `cancel-in-progress: true` to reduce redundant in-flight runs per branch.
- Push validation now runs as a single runner-compatible job; the separate open-PR precheck job was removed due workflow-engine incompatibility.
- PR validation now checks that `coverage.out` exists before computing coverage metadata; when missing, coverage output steps are skipped with a summary note instead of failing the workflow.
- PR decoration is now `continue-on-error` to avoid hard-failing validation when the external `decorate-pr` action's internal extractor step is unavailable.
- PR validation now skips external PR decoration on non-GitHub runners and writes a summary note instead, avoiding runner-specific action resolution failures.
- Coverage summary generation is now resilient when badge outputs or `coverage-packages.md` are unavailable, preventing summary-step hard failures after earlier skips.
- Push and PR validation no longer depend on external `vociferate/coverage-badge` action fetches, avoiding pipeline failures during external TLS/certificate outages.
- Release automation no longer depends on external `vociferate` action fetches; local repository scripts now prepare tags from `CHANGELOG.md` and publish Gitea releases directly via the API, avoiding TLS/certificate outages on the external action host.
- `prepare-release.yml` now quotes the job-level `if:` expression guarding release-preparation commits, fixing YAML parsing errors caused by the colon in the release commit message prefix.
- README badge link target updated to `actions/runs/latest?workflow=...` format per workflow standards.
- CI security scanning now uses GitHub Marketplace actions (`securego/gosec` and `golang/govulncheck-action`) instead of manual tool installation, improving reliability and caching.
- CI setup compatibility fix: gosec scanner now references the correct public action source (`securego/gosec`), resolving action clone failures in Gitea runners.

View File

@@ -1,4 +1,4 @@
# homesick
# gosick
[![Main Validation](https://git.hrafn.xyz/aether/gosick/actions/workflows/push-validation.yml/badge.svg?branch=main&event=push)](https://git.hrafn.xyz/aether/gosick/actions/runs/latest?workflow=push-validation.yml&branch=main&event=push)
[![Coverage](https://s3.hrafn.xyz/aether-workflow-report-artefacts/gosick/branch/main/coverage-badge.svg)](https://s3.hrafn.xyz/aether-workflow-report-artefacts/gosick/branch/main/coverage.html)

View File

@@ -299,7 +299,8 @@ func (a *App) Open(castle string) error {
}
castleRoot := filepath.Join(a.ReposDir, castle)
cmd := exec.Command(editor, ".") // #nosec G204 EDITOR environment variable is user-set
// #nosec G702,G204 -- EDITOR is user-controlled local configuration and command is executed directly without a shell.
cmd := exec.Command(editor, ".")
cmd.Dir = castleRoot
cmd.Stdout = a.Stdout
cmd.Stderr = a.Stderr
@@ -801,6 +802,7 @@ func matchesIgnoredDir(castleHome string, candidate string, subdirs []string) (b
}
func runGitWithIO(dir string, stdout io.Writer, stderr io.Writer, args ...string) error {
// #nosec G204 -- git is fixed binary; args are internal command parameters for expected git operations.
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Stdout = stdout
@@ -834,6 +836,7 @@ func (a *App) sayStatus(action string, message string) {
}
func gitOutput(dir string, args ...string) (string, error) {
// #nosec G204 -- git is fixed binary; args are internal read-only git query parameters.
cmd := exec.Command("git", args...)
cmd.Dir = dir
out, err := cmd.Output()

132
script/prepare-release.sh Normal file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
cd "$repo_root"
version_file="internal/homesick/version/version.go"
primary_changelog="CHANGELOG.md"
compat_changelog="changelog.md"
current_version="$(sed -n 's/^const String = "\([^"]*\)"$/\1/p' "$version_file")"
if [[ -z "$current_version" ]]; then
echo "Failed to read current version from ${version_file}" >&2
exit 1
fi
bump="$(awk '
BEGIN { in_unreleased = 0; section = ""; has_entries = 0; bump = "" }
/^## \[Unreleased\]/ { in_unreleased = 1; next }
/^## \[/ && in_unreleased { exit }
/^### / && in_unreleased { section = substr($0, 5); next }
in_unreleased && /^- / {
has_entries = 1
if (section == "Breaking" || section == "Removed") {
bump = "major"
} else if (section == "Added") {
if (bump != "major") {
bump = "minor"
}
} else if (bump == "") {
bump = "patch"
}
}
END {
if (!has_entries) {
print "none"
exit
}
if (bump == "") {
bump = "patch"
}
print bump
}
' "$primary_changelog")"
if [[ "$bump" == "none" ]]; then
echo "No unreleased changelog entries found; skipping release preparation."
exit 0
fi
IFS=. read -r major minor patch <<< "$current_version"
case "$bump" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
*)
echo "Unsupported bump type: ${bump}" >&2
exit 1
;;
esac
next_version="${major}.${minor}.${patch}"
tag="v${next_version}"
today="$(date -u +%F)"
if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
echo "Tag ${tag} already exists; skipping release preparation."
exit 0
fi
unreleased_line="$(grep -n '^## \[Unreleased\]' "$primary_changelog" | cut -d: -f1)"
if [[ -z "$unreleased_line" ]]; then
echo "Missing [Unreleased] section in ${primary_changelog}" >&2
exit 1
fi
next_heading_line="$(awk -v start="$unreleased_line" 'NR > start && /^## \[/ { print NR; exit }' "$primary_changelog")"
total_lines="$(wc -l < "$primary_changelog" | tr -d ' ')"
if [[ -z "$next_heading_line" ]]; then
next_heading_line=$((total_lines + 1))
fi
tmp_changelog="$(mktemp)"
{
sed -n "1,$((unreleased_line - 1))p" "$primary_changelog"
echo "## [Unreleased]"
echo
echo "### Breaking"
echo
echo "### Added"
echo
echo "### Changed"
echo
echo "### Fixed"
echo
echo "### Removed"
echo
echo "## [${next_version}] - ${today}"
echo
sed -n "$((unreleased_line + 2)),$((next_heading_line - 1))p" "$primary_changelog"
if (( next_heading_line <= total_lines )); then
sed -n "${next_heading_line},${total_lines}p" "$primary_changelog"
fi
} > "$tmp_changelog"
tmp_version="$(mktemp)"
sed "s/^const String = \".*\"$/const String = \"${next_version}\"/" "$version_file" > "$tmp_version"
mv "$tmp_version" "$version_file"
mv "$tmp_changelog" "$primary_changelog"
cp "$primary_changelog" "$compat_changelog"
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@users.noreply.local"
git add "$version_file" "$primary_changelog" "$compat_changelog"
git commit -m "chore(release): prepare ${tag}"
git tag "$tag"
git push origin HEAD:main
git push origin "$tag"
echo "Prepared ${tag} from ${current_version} with a ${bump} bump."

63
script/publish-release.sh Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
cd "$repo_root"
if [[ -z "${GITHUB_REF_NAME:-}" ]]; then
echo "GITHUB_REF_NAME is required" >&2
exit 1
fi
if [[ -z "${GITHUB_REPOSITORY:-}" || -z "${GITHUB_SERVER_URL:-}" || -z "${GITHUB_TOKEN:-}" ]]; then
echo "GITHUB_REPOSITORY, GITHUB_SERVER_URL, and GITHUB_TOKEN are required" >&2
exit 1
fi
tag="${GITHUB_REF_NAME}"
version="${tag#v}"
notes_file="$(mktemp)"
awk -v version="$version" '
$0 ~ ("^## \\\[" version "\\\] - ") { in_section = 1 }
/^## \[/ && in_section && $0 !~ ("^## \\\[" version "\\\] - ") { exit }
in_section { print }
' CHANGELOG.md > "$notes_file"
if [[ ! -s "$notes_file" ]]; then
printf '## [%s]\n\n- Release %s\n' "$version" "$tag" > "$notes_file"
fi
payload_file="$(mktemp)"
jq -n \
--arg tag "$tag" \
--arg name "$tag" \
--arg target "main" \
--rawfile body "$notes_file" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, draft: false, prerelease: false}' > "$payload_file"
api_base="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
status="$(curl -sS -o /tmp/release_lookup.json -w '%{http_code}' \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H 'accept: application/json' \
"${api_base}/releases/tags/${tag}" || true)"
if [[ "$status" == "200" ]]; then
release_id="$(jq -r '.id' /tmp/release_lookup.json)"
curl -fsSL -X PATCH \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H 'accept: application/json' \
-H 'content-type: application/json' \
--data @"$payload_file" \
"${api_base}/releases/${release_id}" >/dev/null
echo "Updated release ${tag}."
else
curl -fsSL -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H 'accept: application/json' \
-H 'content-type: application/json' \
--data @"$payload_file" \
"${api_base}/releases" >/dev/null
echo "Created release ${tag}."
fi