feat(ci): auto-merge pipeline, org gate, and QA fix hints (#284)
* refactor(core): decompose Core into serviceManager + messageBus (#215) Extract two focused, unexported components from the Core "god object": - serviceManager: owns service registry, lifecycle tracking (startables/ stoppables), and service lock - messageBus: owns IPC action dispatch, query handling, and task handling All public API methods on Core become one-line delegation wrappers. Zero consumer changes — no files outside pkg/framework/core/ modified. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(core): remove unused fields from test struct Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(core): address review feedback from Gemini and Copilot - Move locked check inside mutex in registerService to fix TOCTOU race - Add mutex guards to enableLock and applyLock methods - Replace fmt.Errorf with errors.Join in action() for correct error aggregation (consistent with queryAll and lifecycle methods) - Add TestMessageBus_Action_Bad for error aggregation coverage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ci(workflows): bump host-uk/build from v3 to v4 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ci(workflows): replace Wails build with Go CLI build The build action doesn't yet support Wails v3. Comment out the GUI build step and use host-uk/build/actions/setup/go for Go toolchain setup with a plain `go build` for the CLI binary. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(container): check context before select in Stop to fix flaky test Stop() now checks ctx.Err() before entering the select block. When a pre-cancelled context is passed, the select could non-deterministically choose <-done over <-ctx.Done() if the process had already exited, causing TestLinuxKitManager_Stop_Good_ContextCancelled to fail on CI. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ci): trim CodeQL matrix to valid languages Remove javascript-typescript and actions from CodeQL matrix — this repo contains only Go and Python. Invalid languages blocked SARIF upload and prevented merge. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(go): add `core go fuzz` command and wire into QA - New `core go fuzz` command discovers Fuzz* targets and runs them with configurable --duration (default 10s per target) - Fuzz added to default QA checks with 5s burst duration - Seed fuzz targets for core package: FuzzE (error constructor), FuzzServiceRegistration, FuzzMessageDispatch Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ci(codeql): add workflow_dispatch trigger for manual runs Allows manual triggering of CodeQL when the automatic pull_request trigger doesn't fire. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ci(codeql): remove workflow in favour of default setup CodeQL default setup is now enabled via repo settings for go and python. The workflow-based approach uploaded results as "code quality" rather than "code scanning", which didn't satisfy the code_scanning ruleset requirement. Default setup handles this natively. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ci(workflows): add explicit permissions to all workflows - agent-verify: add issues: write (was missing, writes comments/labels) - ci: add contents: read (explicit least-privilege) - coverage: add contents: read (explicit least-privilege) All workflows now declare permissions explicitly. Repo default is read-only, so workflows without a block silently lacked write access. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ci(workflows): replace inline logic with org reusable workflow callers agent-verify.yml and auto-project.yml now delegate to centralised reusable workflows in host-uk/.github, reducing per-repo duplication. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(ci): auto-merge pipeline, org gate, and QA fix hints Add auto-merge workflow for org member PRs, external PR gate with label-based approval, and actionable fix instructions for QA failures. - auto-merge.yml: enable squash auto-merge for org member PRs - pr-gate.yml: org-gate check blocks external PRs without label - cmd_qa.go: add FixHint field, fixHintFor(), extractFailingTest() - Ruleset: thread resolution, stale review dismissal, 1min merge wait Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b806f4f11c
commit
90531c148d
3 changed files with 130 additions and 0 deletions
40
.github/workflows/auto-merge.yml
vendored
Normal file
40
.github/workflows/auto-merge.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
name: Auto Merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
if: "!github.event.pull_request.draft"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check org membership and enable auto-merge
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const author = context.payload.pull_request.user.login;
|
||||
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: owner,
|
||||
username: author,
|
||||
});
|
||||
} catch {
|
||||
core.info(`${author} is not an org member — skipping auto-merge`);
|
||||
return;
|
||||
}
|
||||
|
||||
await exec.exec('gh', [
|
||||
'pr', 'merge', process.env.PR_NUMBER,
|
||||
'--auto', '--squash',
|
||||
]);
|
||||
core.info(`Auto-merge enabled for #${process.env.PR_NUMBER}`);
|
||||
42
.github/workflows/pr-gate.yml
vendored
Normal file
42
.github/workflows/pr-gate.yml
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
name: PR Gate
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
org-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check org membership or approval label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const author = context.payload.pull_request.user.login;
|
||||
|
||||
// Check if author is an org member
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: owner,
|
||||
username: author,
|
||||
});
|
||||
core.info(`${author} is an org member — gate passed`);
|
||||
return;
|
||||
} catch {
|
||||
core.info(`${author} is not an org member — checking for label`);
|
||||
}
|
||||
|
||||
// Check for external-approved label
|
||||
const labels = context.payload.pull_request.labels.map(l => l.name);
|
||||
if (labels.includes('external-approved')) {
|
||||
core.info('external-approved label present — gate passed');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setFailed(
|
||||
`External PR from ${author} requires an org member to add the "external-approved" label before merge.`
|
||||
);
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -147,6 +148,7 @@ type CheckResult struct {
|
|||
Duration string `json:"duration"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
FixHint string `json:"fix_hint,omitempty"`
|
||||
}
|
||||
|
||||
func runGoQA(cmd *cli.Command, args []string) error {
|
||||
|
|
@ -218,6 +220,7 @@ func runGoQA(cmd *cli.Command, args []string) error {
|
|||
if qaVerbose {
|
||||
result.Output = output
|
||||
}
|
||||
result.FixHint = fixHintFor(check.Name, output)
|
||||
failed++
|
||||
|
||||
if !qaJSON && !qaQuiet {
|
||||
|
|
@ -225,6 +228,9 @@ func runGoQA(cmd *cli.Command, args []string) error {
|
|||
if qaVerbose && output != "" {
|
||||
cli.Text(output)
|
||||
}
|
||||
if result.FixHint != "" {
|
||||
cli.Hint("fix", result.FixHint)
|
||||
}
|
||||
}
|
||||
|
||||
if qaFailFast {
|
||||
|
|
@ -260,6 +266,7 @@ func runGoQA(cmd *cli.Command, args []string) error {
|
|||
if !qaJSON && !qaQuiet {
|
||||
cli.Print(" %s Coverage %.1f%% below threshold %.1f%%\n",
|
||||
cli.ErrorStyle.Render(cli.Glyph(":cross:")), cov, qaThreshold)
|
||||
cli.Hint("fix", "Run 'core go cov --open' to see uncovered lines, then add tests.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -436,6 +443,47 @@ func buildCheck(name string) QACheck {
|
|||
}
|
||||
}
|
||||
|
||||
// fixHintFor returns an actionable fix instruction for a given check failure.
|
||||
func fixHintFor(checkName, output string) string {
|
||||
switch checkName {
|
||||
case "format", "fmt":
|
||||
return "Run 'core go qa fmt --fix' to auto-format."
|
||||
case "vet":
|
||||
return "Fix the issues reported by go vet — typically genuine bugs."
|
||||
case "lint":
|
||||
return "Run 'core go qa lint --fix' for auto-fixable issues."
|
||||
case "test":
|
||||
if name := extractFailingTest(output); name != "" {
|
||||
return fmt.Sprintf("Run 'go test -run %s -v ./...' to debug.", name)
|
||||
}
|
||||
return "Run 'go test -run <TestName> -v ./path/' to debug."
|
||||
case "race":
|
||||
return "Data race detected. Add mutex, channel, or atomic to synchronise shared state."
|
||||
case "bench":
|
||||
return "Benchmark regression. Run 'go test -bench=. -benchmem' to reproduce."
|
||||
case "vuln":
|
||||
return "Run 'govulncheck ./...' for details. Update affected deps with 'go get -u'."
|
||||
case "sec":
|
||||
return "Review gosec findings. Common fixes: validate inputs, parameterised queries."
|
||||
case "fuzz":
|
||||
return "Add a regression test for the crashing input in testdata/fuzz/<Target>/."
|
||||
case "docblock":
|
||||
return "Add doc comments to exported symbols: '// Name does X.' before each declaration."
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var failTestRe = regexp.MustCompile(`--- FAIL: (\w+)`)
|
||||
|
||||
// extractFailingTest parses the first failing test name from go test output.
|
||||
func extractFailingTest(output string) string {
|
||||
if m := failTestRe.FindStringSubmatch(output); len(m) > 1 {
|
||||
return m[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func runCheckCapture(ctx context.Context, dir string, check QACheck) (string, error) {
|
||||
// Handle internal checks
|
||||
if check.Command == "_internal_" {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue