Skip to content

Commit 619dee6

Browse files
authored
ci: 增加 GitHub readiness 复核脚本
1 parent 84e8861 commit 619dee6

2 files changed

Lines changed: 223 additions & 0 deletions

File tree

github_readiness_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package readiness
2+
3+
import (
4+
"os"
5+
"strings"
6+
"testing"
7+
)
8+
9+
const projectName = "mark2note"
10+
11+
var releaseSuffixes = []string{"darwin-amd64", "darwin-arm64", "linux-amd64", "linux-arm64"}
12+
13+
func TestGitHubCIWorkflowReadiness(t *testing.T) {
14+
content := readTextFile(t, ".github/workflows/ci.yml")
15+
for _, want := range []string{
16+
"name: CI",
17+
"push:",
18+
"pull_request:",
19+
"actions/checkout@v6",
20+
"fetch-depth: 0",
21+
"actions/setup-go@v6",
22+
"go-version-file: go.mod",
23+
"scripts/secret-scan.sh",
24+
"gofmt -l",
25+
"go vet ./...",
26+
"go test ./...",
27+
} {
28+
assertContains(t, content, want)
29+
}
30+
}
31+
32+
func TestGitHubReleaseWorkflowReadiness(t *testing.T) {
33+
content := readTextFile(t, ".github/workflows/release.yml")
34+
for _, want := range []string{
35+
"name: Release",
36+
"tags:",
37+
"v*",
38+
"contents: write",
39+
"preflight:",
40+
"build:",
41+
"release:",
42+
"needs: preflight",
43+
"needs: build",
44+
"actions/checkout@v6",
45+
"fetch-depth: 0",
46+
"actions/setup-go@v6",
47+
"go-version-file: go.mod",
48+
"scripts/ci-local.sh clean",
49+
"package=\"" + projectName + "-${SUFFIX}\"",
50+
"actions/upload-artifact@v7",
51+
"actions/download-artifact@v8",
52+
"sha256sum",
53+
"checksums.txt",
54+
"gh release view",
55+
"gh release upload",
56+
"gh release create",
57+
"GH_TOKEN",
58+
} {
59+
assertContains(t, content, want)
60+
}
61+
for _, suffix := range releaseSuffixes {
62+
assertContains(t, content, suffix)
63+
}
64+
for _, forbidden := range []string{
65+
"secrets.",
66+
"softprops/action-gh-release",
67+
"GoReleaser",
68+
"goreleaser",
69+
"git push",
70+
"git tag",
71+
} {
72+
assertNotContains(t, content, forbidden)
73+
}
74+
}
75+
76+
func TestGitHubCodeQLWorkflowReadiness(t *testing.T) {
77+
content := readTextFile(t, ".github/workflows/codeql.yml")
78+
for _, want := range []string{
79+
"name: CodeQL",
80+
"push:",
81+
"pull_request:",
82+
"schedule:",
83+
"security-events: write",
84+
"actions/checkout@v6",
85+
"github/codeql-action/init@v4",
86+
"languages: go",
87+
"github/codeql-action/autobuild@v4",
88+
"github/codeql-action/analyze@v4",
89+
} {
90+
assertContains(t, content, want)
91+
}
92+
}
93+
94+
func TestGitHubReadinessScriptUsesDedicatedEndpoints(t *testing.T) {
95+
path := "scripts/github-readiness.sh"
96+
content := readTextFile(t, path)
97+
info, err := os.Stat(path)
98+
if err != nil {
99+
t.Fatalf("expected %s to exist: %v", path, err)
100+
}
101+
if info.Mode().Perm()&0o111 == 0 {
102+
t.Fatalf("expected %s to be executable", path)
103+
}
104+
for _, want := range []string{
105+
"gh api \"repos/${repo}\"",
106+
"security_and_analysis.secret_scanning.status",
107+
"security_and_analysis.secret_scanning_push_protection.status",
108+
"gh api \"repos/${repo}/private-vulnerability-reporting\"",
109+
"private-vulnerability-reporting",
110+
"branches/${default_branch}/protection",
111+
"code-scanning/analyses",
112+
} {
113+
assertContains(t, content, want)
114+
}
115+
assertNotContains(t, content, "security_and_analysis.private")
116+
}
117+
118+
func readTextFile(t *testing.T, path string) string {
119+
t.Helper()
120+
content, err := os.ReadFile(path)
121+
if err != nil {
122+
t.Fatalf("expected %s to exist: %v", path, err)
123+
}
124+
return string(content)
125+
}
126+
127+
func assertContains(t *testing.T, content, want string) {
128+
t.Helper()
129+
if !strings.Contains(content, want) {
130+
t.Fatalf("expected content to contain %q", want)
131+
}
132+
}
133+
134+
func assertNotContains(t *testing.T, content, forbidden string) {
135+
t.Helper()
136+
if strings.Contains(content, forbidden) {
137+
t.Fatalf("expected content not to contain %q", forbidden)
138+
}
139+
}

scripts/github-readiness.sh

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
usage() {
5+
printf 'usage: %s [owner/repo]\n' "$0" >&2
6+
}
7+
8+
if [[ $# -gt 1 || "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
9+
usage
10+
exit 2
11+
fi
12+
13+
if ! command -v gh >/dev/null 2>&1; then
14+
printf 'missing required command: gh\n' >&2
15+
exit 127
16+
fi
17+
18+
repo="${1:-}"
19+
if [[ -z "$repo" ]]; then
20+
repo="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
21+
fi
22+
23+
if [[ "$repo" != */* ]]; then
24+
printf 'repository must use owner/repo format: %s\n' "$repo" >&2
25+
exit 2
26+
fi
27+
28+
failed=0
29+
30+
status_line() {
31+
local label="$1"
32+
local value="$2"
33+
printf '%-38s %s\n' "$label:" "$value"
34+
}
35+
36+
require_enabled() {
37+
local label="$1"
38+
local value="$2"
39+
if [[ "$value" == "enabled" || "$value" == "true" ]]; then
40+
status_line "$label" "$value"
41+
return
42+
fi
43+
status_line "$label" "$value"
44+
failed=1
45+
}
46+
47+
default_branch="$(gh api "repos/${repo}" --jq '.default_branch')"
48+
secret_scanning="$(gh api "repos/${repo}" --jq '.security_and_analysis.secret_scanning.status // "unavailable"')"
49+
push_protection="$(gh api "repos/${repo}" --jq '.security_and_analysis.secret_scanning_push_protection.status // "unavailable"')"
50+
private_vulnerability_reporting="$(gh api "repos/${repo}/private-vulnerability-reporting" --jq '.enabled')"
51+
52+
if required_checks="$(gh api "repos/${repo}/branches/${default_branch}/protection" --jq '.required_status_checks.contexts // [] | join(", ")' 2>/dev/null)"; then
53+
branch_protection="enabled"
54+
else
55+
branch_protection="unavailable or disabled"
56+
required_checks=""
57+
fi
58+
59+
code_scanning_tools="$(gh api "repos/${repo}/code-scanning/analyses" --jq '.[].tool.name' 2>/dev/null | sort -u || true)"
60+
if [[ -n "$code_scanning_tools" ]]; then
61+
code_scanning="enabled"
62+
else
63+
code_scanning="unavailable or no analyses"
64+
failed=1
65+
fi
66+
67+
status_line "Repository" "$repo"
68+
status_line "Default branch" "$default_branch"
69+
require_enabled "Secret scanning" "$secret_scanning"
70+
require_enabled "Push protection" "$push_protection"
71+
require_enabled "Private vulnerability reporting" "$private_vulnerability_reporting"
72+
require_enabled "Branch protection" "$branch_protection"
73+
if [[ -n "$required_checks" ]]; then
74+
status_line "Required status checks" "$required_checks"
75+
else
76+
status_line "Required status checks" "none reported"
77+
failed=1
78+
fi
79+
status_line "Code scanning" "$code_scanning"
80+
if [[ -n "$code_scanning_tools" ]]; then
81+
status_line "Code scanning tools" "$code_scanning_tools"
82+
fi
83+
84+
exit "$failed"

0 commit comments

Comments
 (0)