mirror of
https://github.com/lukaszraczylo/shared-actions.git
synced 2026-10-12 04:54:48 +00:00
The coverage step matched every line that contains the word total, so a function name such as subtotal added a second value. GitHub then rejected the multi-line step output. The step now reads the line that starts with total:.
416 lines
15 KiB
YAML
416 lines
15 KiB
YAML
name: Go Pull Request
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
go-version:
|
|
description: "Go version to use"
|
|
required: false
|
|
type: string
|
|
default: ">=1.24"
|
|
coverage-threshold:
|
|
description: "Minimum coverage percentage required (0 to disable)"
|
|
required: false
|
|
type: number
|
|
default: 0
|
|
lfs:
|
|
description: "Enable Git LFS checkout (for repos with large files)"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
build-tags:
|
|
description: "Build tags to pass to go test (e.g., 'fts5')"
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
|
|
runner:
|
|
description: "Runner label, or JSON object such as {\"group\":\"ci\",\"labels\":[\"linux\"]} to select a runner group. Falls back to vars.SHARED_ACTIONS_RUNNER, then ubuntu-latest."
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
|
|
# Caller must declare these permissions:
|
|
# permissions:
|
|
# contents: read
|
|
# pull-requests: write
|
|
# security-events: write
|
|
|
|
concurrency:
|
|
group: go-pr-${{ github.repository }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
pre:
|
|
name: Detect duplicate run
|
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
|
timeout-minutes: 5
|
|
outputs:
|
|
skip: ${{ steps.dup.outputs.skip }}
|
|
steps:
|
|
# A push to a branch with an open PR also fires pull_request; run only the PR one.
|
|
- name: Skip push run when a PR is open
|
|
id: dup
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
let skip = false;
|
|
if (context.eventName === 'push') {
|
|
try {
|
|
const { data } = await github.rest.pulls.list({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
head: `${context.repo.owner}:${context.ref.replace('refs/heads/', '')}`,
|
|
state: 'open',
|
|
});
|
|
skip = data.length > 0;
|
|
} catch (e) {
|
|
core.warning(`PR lookup failed, running checks: ${e.message}`);
|
|
}
|
|
}
|
|
core.setOutput('skip', String(skip));
|
|
if (skip) core.notice('Open PR found for this branch; checks run on the pull_request event.');
|
|
static-analysis:
|
|
name: Static Analysis
|
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
|
needs: pre
|
|
if: needs.pre.outputs.skip != 'true'
|
|
timeout-minutes: 15
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
lfs: ${{ inputs.lfs }}
|
|
|
|
- name: Run workflow prepare script
|
|
shell: bash
|
|
run: |
|
|
if [ -f "./workflow-prepare.sh" ]; then
|
|
chmod +x ./workflow-prepare.sh
|
|
./workflow-prepare.sh
|
|
fi
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: ${{ inputs.go-version }}
|
|
cache: true
|
|
|
|
- name: Install staticcheck
|
|
run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
|
|
|
- name: Register problem matcher
|
|
shell: bash
|
|
run: |
|
|
cat > "$RUNNER_TEMP/go-problem-matcher.json" <<'JSON'
|
|
{"problemMatcher":[{"owner":"go-lint","severity":"error","pattern":[{"regexp":"^(?:vet: )?([^\\s:]+\\.go):(\\d+):(\\d+): (.+)$","file":1,"line":2,"column":3,"message":4}]}]}
|
|
JSON
|
|
echo "::add-matcher::$RUNNER_TEMP/go-problem-matcher.json"
|
|
|
|
- name: Run go vet
|
|
run: go vet ./...
|
|
|
|
- name: Run staticcheck
|
|
run: staticcheck ./...
|
|
|
|
security-scan:
|
|
name: Security Scan
|
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
|
needs: pre
|
|
if: needs.pre.outputs.skip != 'true'
|
|
timeout-minutes: 20
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
lfs: ${{ inputs.lfs }}
|
|
|
|
- name: Run workflow prepare script
|
|
shell: bash
|
|
run: |
|
|
if [ -f "./workflow-prepare.sh" ]; then
|
|
chmod +x ./workflow-prepare.sh
|
|
./workflow-prepare.sh
|
|
fi
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: ${{ inputs.go-version }}
|
|
cache: true
|
|
|
|
- name: Run TruffleHog
|
|
uses: trufflesecurity/trufflehog@main
|
|
with:
|
|
extra_args: --only-verified
|
|
|
|
- name: Install govulncheck
|
|
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
|
|
- name: Run govulncheck
|
|
run: |
|
|
# Run govulncheck and capture output
|
|
set +e
|
|
OUTPUT=$(govulncheck -show verbose ./... 2>&1)
|
|
EXIT_CODE=$?
|
|
echo "$OUTPUT"
|
|
|
|
# If there are vulnerabilities, check if they're only in stdlib
|
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
# Check if there are any third-party package vulnerabilities
|
|
# Stdlib vulnerabilities show "Found in: <pkg>@go<version>" (e.g., crypto/[email protected])
|
|
# Third-party vulns show "Found in: <pkg>@v<version>" (e.g., github.com/foo/[email protected])
|
|
if echo "$OUTPUT" | grep -q "Found in:"; then
|
|
# If no "Found in:" lines contain @v (third-party version), only stdlib affected
|
|
if ! echo "$OUTPUT" | grep "Found in:" | grep -qE "@v[0-9]"; then
|
|
echo ""
|
|
echo "⚠️ Only stdlib vulnerabilities found - these require a Go version upgrade"
|
|
echo "✅ No vulnerabilities in third-party dependencies"
|
|
exit 0
|
|
fi
|
|
fi
|
|
exit $EXIT_CODE
|
|
fi
|
|
|
|
gosec:
|
|
name: Gosec SARIF
|
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
|
needs: pre
|
|
if: needs.pre.outputs.skip != 'true'
|
|
timeout-minutes: 15
|
|
permissions:
|
|
contents: read
|
|
security-events: write
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
lfs: ${{ inputs.lfs }}
|
|
|
|
- name: Run workflow prepare script
|
|
shell: bash
|
|
run: |
|
|
if [ -f "./workflow-prepare.sh" ]; then
|
|
chmod +x ./workflow-prepare.sh
|
|
./workflow-prepare.sh
|
|
fi
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: ${{ inputs.go-version }}
|
|
cache: true
|
|
|
|
- name: Install gosec
|
|
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
|
|
|
|
- name: Run gosec
|
|
run: gosec -no-fail -fmt sarif -out gosec-results.sarif ./... || true
|
|
|
|
- name: Upload gosec SARIF
|
|
if: always() && hashFiles('gosec-results.sarif') != ''
|
|
uses: github/codeql-action/upload-sarif@v4
|
|
with:
|
|
sarif_file: gosec-results.sarif
|
|
category: gosec
|
|
continue-on-error: true
|
|
|
|
goreleaser-check:
|
|
name: GoReleaser Config Validation
|
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
|
needs: pre
|
|
if: needs.pre.outputs.skip != 'true'
|
|
timeout-minutes: 10
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
lfs: ${{ inputs.lfs }}
|
|
|
|
- name: Run workflow prepare script
|
|
shell: bash
|
|
run: |
|
|
if [ -f "./workflow-prepare.sh" ]; then
|
|
chmod +x ./workflow-prepare.sh
|
|
./workflow-prepare.sh
|
|
fi
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: ${{ inputs.go-version }}
|
|
cache: true
|
|
|
|
- name: Check goreleaser config
|
|
if: hashFiles('.goreleaser.yml', '.goreleaser.yaml') != ''
|
|
uses: goreleaser/goreleaser-action@v7
|
|
with:
|
|
distribution: goreleaser
|
|
version: "~> v2"
|
|
args: check
|
|
|
|
codeql:
|
|
name: CodeQL Analysis
|
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
|
needs: pre
|
|
if: needs.pre.outputs.skip != 'true'
|
|
timeout-minutes: 45
|
|
permissions:
|
|
contents: read
|
|
security-events: write
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
lfs: ${{ inputs.lfs }}
|
|
|
|
- name: Run workflow prepare script
|
|
shell: bash
|
|
run: |
|
|
if [ -f "./workflow-prepare.sh" ]; then
|
|
chmod +x ./workflow-prepare.sh
|
|
./workflow-prepare.sh
|
|
fi
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: ${{ inputs.go-version }}
|
|
cache: true
|
|
|
|
- name: Initialize CodeQL
|
|
uses: github/codeql-action/init@v4
|
|
with:
|
|
languages: go
|
|
|
|
- name: Autobuild
|
|
uses: github/codeql-action/autobuild@v4
|
|
|
|
- name: Perform CodeQL Analysis
|
|
uses: github/codeql-action/analyze@v4
|
|
|
|
test:
|
|
name: Tests & Coverage
|
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
|
needs: pre
|
|
if: needs.pre.outputs.skip != 'true'
|
|
timeout-minutes: 30
|
|
outputs:
|
|
coverage: ${{ steps.coverage.outputs.coverage }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
lfs: ${{ inputs.lfs }}
|
|
|
|
- name: Run workflow prepare script
|
|
shell: bash
|
|
run: |
|
|
if [ -f "./workflow-prepare.sh" ]; then
|
|
chmod +x ./workflow-prepare.sh
|
|
./workflow-prepare.sh
|
|
fi
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: ${{ inputs.go-version }}
|
|
cache: true
|
|
|
|
- name: Run tests with coverage
|
|
env:
|
|
CGO_ENABLED: 1
|
|
run: |
|
|
TAGS="${{ inputs.build-tags }}"
|
|
if [ -n "$TAGS" ]; then
|
|
go test -tags "$TAGS" -race -coverprofile=coverage.out -covermode=atomic ./...
|
|
else
|
|
go test -race -coverprofile=coverage.out -covermode=atomic ./...
|
|
fi
|
|
go tool cover -func=coverage.out -o=coverage.txt
|
|
|
|
- name: Calculate coverage
|
|
id: coverage
|
|
run: |
|
|
COVERAGE=$(go tool cover -func=coverage.out | awk '/^total:/ {print $3}' | sed 's/%//')
|
|
echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
|
|
echo "Total Coverage: $COVERAGE%"
|
|
|
|
report:
|
|
name: PR Checks Report
|
|
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
|
|
needs: [pre, static-analysis, security-scan, gosec, goreleaser-check, codeql, test]
|
|
if: ${{ !cancelled() && needs.pre.outputs.skip != 'true' }}
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
steps:
|
|
- name: Summarise, comment and gate
|
|
uses: actions/github-script@v9
|
|
env:
|
|
NEEDS: ${{ toJSON(needs) }}
|
|
THRESHOLD: ${{ inputs.coverage-threshold }}
|
|
with:
|
|
script: |
|
|
const needs = JSON.parse(process.env.NEEDS);
|
|
const threshold = Number(process.env.THRESHOLD);
|
|
const labels = {
|
|
'static-analysis': 'Static analysis (vet, staticcheck)',
|
|
'security-scan': 'Security scan (TruffleHog, govulncheck)',
|
|
'gosec': 'Gosec',
|
|
'goreleaser-check': 'GoReleaser config',
|
|
'codeql': 'CodeQL',
|
|
'test': 'Tests (race) & coverage',
|
|
};
|
|
const icon = { success: '✅', failure: '❌', cancelled: '🚫', skipped: '⏭️' };
|
|
const failed = [];
|
|
let body = '<!-- go-pr-report -->\n## PR checks\n\n| Check | Result |\n|---|---|\n';
|
|
for (const [key, label] of Object.entries(labels)) {
|
|
const result = needs[key]?.result ?? 'skipped';
|
|
if (result === 'failure' || result === 'cancelled') failed.push(label);
|
|
body += `| ${label} | ${icon[result] ?? result} ${result} |\n`;
|
|
}
|
|
|
|
const coverage = parseFloat(needs.test?.outputs?.coverage);
|
|
const hasCoverage = !Number.isNaN(coverage);
|
|
if (hasCoverage) {
|
|
body += `\n**Total coverage:** ${coverage}%`;
|
|
if (threshold > 0) {
|
|
const ok = coverage >= threshold;
|
|
body += ` (threshold ${threshold}%: ${ok ? '✅ met' : '❌ below'})`;
|
|
if (!ok) failed.push(`Coverage ${coverage}% below ${threshold}%`);
|
|
}
|
|
body += '\n';
|
|
}
|
|
body += `\n[Workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
|
|
|
|
await core.summary.addRaw(body, true).write();
|
|
|
|
if (context.eventName === 'pull_request') {
|
|
try {
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number,
|
|
});
|
|
const existing = comments.find(c => c.user.type === 'Bot' &&
|
|
(c.body.includes('<!-- go-pr-report -->') || c.body.includes('Test Coverage Report')));
|
|
const args = { owner: context.repo.owner, repo: context.repo.repo, body };
|
|
if (existing) {
|
|
await github.rest.issues.updateComment({ ...args, comment_id: existing.id });
|
|
} else {
|
|
await github.rest.issues.createComment({ ...args, issue_number: context.issue.number });
|
|
}
|
|
} catch (e) {
|
|
core.warning(`Could not post PR comment (fork PR or missing pull-requests: write): ${e.message}`);
|
|
}
|
|
}
|
|
|
|
if (failed.length) core.setFailed(`Failed: ${failed.join('; ')}`);
|