chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:54 +08:00
commit bf9395e022
2349 changed files with 588574 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
/internal/ @liangshuo-1
# Last match wins: existing domains below are exempt, only new skills/ entries need review.
/skills/ @liangshuo-1
/skills/lark-approval/
/skills/lark-apps/
/skills/lark-attendance/
/skills/lark-base/
/skills/lark-calendar/
/skills/lark-contact/
/skills/lark-doc/
/skills/lark-drive/
/skills/lark-event/
/skills/lark-im/
/skills/lark-mail/
/skills/lark-markdown/
/skills/lark-minutes/
/skills/lark-okr/
/skills/lark-openapi-explorer/
/skills/lark-shared/
/skills/lark-sheets/
/skills/lark-skill-maker/
/skills/lark-slides/
/skills/lark-task/
/skills/lark-vc/
/skills/lark-vc-agent/
/skills/lark-whiteboard/
/skills/lark-wiki/
/skills/lark-workflow-meeting-summary/
/skills/lark-workflow-standup-report/
+16
View File
@@ -0,0 +1,16 @@
## Summary
<!-- Briefly describe the motivation and scope of this change in 1-3 sentences. -->
## Changes
<!-- List the main changes in this PR. -->
- Change 1
- Change 2
## Test Plan
<!-- Describe how this change was verified. -->
- [ ] Unit tests pass
- [ ] Manual local verification confirms the `lark-cli <domain> <command>` flow works as expected
## Related Issues
<!-- Link related issues. Use Closes/Fixes to close them automatically. -->
- None
+116
View File
@@ -0,0 +1,116 @@
name: Architecture Audit
on:
schedule:
- cron: '0 9 * * 1' # Monday 09:00 UTC
workflow_dispatch:
permissions:
contents: read
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Fetch meta data
run: python3 scripts/fetch_meta.py
- name: Dead code detection
run: |
echo "## Dead Code" >> report.md
go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./... 2>&1 | tee deadcode.txt
count=$(wc -l < deadcode.txt | tr -d ' ')
echo "Found **$count** unreachable functions" >> report.md
echo '```' >> report.md
cat deadcode.txt >> report.md
echo '```' >> report.md
- name: Package complexity
run: |
echo "## Package Complexity" >> report.md
echo "" >> report.md
echo "Packages exceeding 2 000 lines or 20 files:" >> report.md
echo "" >> report.md
echo "| Package | Files | Lines | Deps |" >> report.md
echo "|---------|-------|-------|------|" >> report.md
found=0
for pkg in $(go list ./cmd/... ./internal/... ./shortcuts/...); do
dir=$(go list -f '{{.Dir}}' "$pkg")
files=$(find "$dir" -maxdepth 1 -name '*.go' ! -name '*_test.go' | wc -l | tr -d ' ')
lines=$(find "$dir" -maxdepth 1 -name '*.go' ! -name '*_test.go' -exec cat {} + 2>/dev/null | wc -l | tr -d ' ')
deps=$(go list -f '{{len .Imports}}' "$pkg")
if [ "$lines" -gt 2000 ] || [ "$files" -gt 20 ]; then
echo "| **$pkg** | **$files** | **$lines** | **$deps** |" >> report.md
found=1
fi
done
if [ "$found" = "0" ]; then
echo "| _(none)_ | | | |" >> report.md
fi
- name: Dependency freshness
run: |
echo "## Outdated Dependencies" >> report.md
echo '```' >> report.md
go list -m -u all 2>/dev/null | grep '\[' >> report.md || echo "All dependencies up to date" >> report.md
echo '```' >> report.md
- name: Circular dependency check
run: |
echo "## Circular Dependencies" >> report.md
go list -f '{{.ImportPath}} {{join .Imports " "}}' ./... | \
go run golang.org/x/tools/cmd/digraph@v0.31.0 scc 2>&1 | tee cycles.txt
if [ -s cycles.txt ]; then
echo '```' >> report.md
cat cycles.txt >> report.md
echo '```' >> report.md
else
echo "No circular dependencies detected." >> report.md
fi
- name: E2E coverage gaps
run: |
echo "## E2E Coverage Gaps" >> report.md
echo "" >> report.md
echo "Shortcut domains without E2E tests:" >> report.md
echo "" >> report.md
found=0
for domain in $(ls -d shortcuts/*/); do
name=$(basename "$domain")
if [ "$name" = "common" ]; then continue; fi
if [ ! -d "tests/cli_e2e/$name" ]; then
echo "- **$name** (no tests/cli_e2e/$name/)" >> report.md
found=1
fi
done
if [ "$found" = "0" ]; then
echo "All shortcut domains have E2E test directories." >> report.md
fi
- name: Coverage
run: |
echo "## Coverage" >> report.md
packages=$(go list ./... | grep -v 'tests/cli_e2e')
go test -coverprofile=coverage.txt -covermode=atomic $packages 2>/dev/null || true
total=$(go tool cover -func=coverage.txt 2>/dev/null | grep total | awk '{print $3}')
echo "Current total coverage: **${total:-n/a}**" >> report.md
- name: Publish report
run: |
echo "# Architecture Audit Report — $(date +%Y-%m-%d)" > $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
cat report.md >> $GITHUB_STEP_SUMMARY
- name: Upload report artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: arch-audit-${{ github.run_number }}
path: report.md
retention-days: 90
+465
View File
@@ -0,0 +1,465 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, edited]
workflow_dispatch:
permissions:
contents: read
actions: read
jobs:
# ── Layer 1: Fast Gate ─────────────────────────────────────────────
fast-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Fetch meta data
run: python3 scripts/fetch_meta.py
- name: Build
run: go build ./...
- name: Vet
run: go vet ./...
- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "$unformatted"
echo "::error::Unformatted Go files detected — run 'gofmt -w .' and commit"
exit 1
fi
- name: Check go.mod tidiness
run: |
go mod tidy
if ! git diff --quiet go.mod go.sum; then
echo "::error::go.mod or go.sum is not tidy. Run 'go mod tidy' and commit the changes."
git diff go.mod go.sum
exit 1
fi
# ── Layer 2: Quality Gate ──────────────────────────────────────────
unit-test:
needs: fast-gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Fetch meta data
run: python3 scripts/fetch_meta.py
- name: Run tests
run: go test -v -race -count=1 -timeout=5m ./cmd/... ./internal/... ./shortcuts/... ./extension/...
lint:
needs: fast-gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Fetch meta data
run: python3 scripts/fetch_meta.py
- name: Resolve changed-from baseline
env:
QUALITY_GATE_CHANGED_FROM: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }}
run: echo "QUALITY_GATE_CHANGED_FROM=$(bash scripts/resolve-changed-from.sh)" >> "$GITHUB_ENV"
- name: Run golangci-lint
run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev="$QUALITY_GATE_CHANGED_FROM"
- name: Run source-contract lint guards (lintcheck)
run: go run -C lint . --changed-from "$QUALITY_GATE_CHANGED_FROM" ..
- name: Run lint module tests
run: go test -C lint ./... -count=1
script-test:
needs: fast-gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Run script tests
run: make script-test
deterministic-gate:
needs: fast-gate
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Fetch meta data
run: python3 scripts/fetch_meta.py
- name: Resolve changed-from baseline
env:
QUALITY_GATE_CHANGED_FROM: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }}
run: echo "QUALITY_GATE_CHANGED_FROM=$(bash scripts/resolve-changed-from.sh)" >> "$GITHUB_ENV"
- name: Write public content metadata
if: ${{ github.event_name == 'pull_request' }}
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_BRANCH: ${{ github.head_ref }}
run: |
mkdir -p .tmp/quality-gate
python3 - <<'PY'
import json
import os
with open(".tmp/quality-gate/public-content-metadata.json", "w", encoding="utf-8") as f:
json.dump({
"title": os.environ.get("PR_TITLE", ""),
"body": os.environ.get("PR_BODY", ""),
"branch": os.environ.get("PR_BRANCH", ""),
}, f)
f.write("\n")
PY
- name: Run CLI deterministic gate
run: PUBLIC_CONTENT_METADATA=.tmp/quality-gate/public-content-metadata.json make quality-gate
- name: Upload quality gate facts
if: ${{ always() && github.event_name == 'pull_request' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: quality-gate-facts-${{ github.event.pull_request.base.sha }}-${{ github.event.pull_request.head.sha }}
path: .tmp/quality-gate/facts.json
if-no-files-found: error
retention-days: 7
coverage:
needs: fast-gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Fetch meta data
run: python3 scripts/fetch_meta.py
- name: Run tests with coverage
run: |
packages=$(go list ./... | grep -v '^github.com/larksuite/cli/tests/cli_e2e$' | grep -v '^github.com/larksuite/cli/tests/cli_e2e/')
go test -race -coverprofile=coverage.txt -covermode=atomic $packages
- name: Upload coverage to Codecov
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }}
uses: codecov/codecov-action@3f20e214133d0983f9a10f3d63b0faf9241a3daa # v6
with:
files: coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
- name: Check coverage threshold
run: |
total=$(go tool cover -func=coverage.txt | grep total | awk '{print $3}' | tr -d '%')
threshold=40
echo "Coverage: ${total}% (threshold: ${threshold}%)"
if (( $(echo "$total < $threshold" | bc -l) )); then
echo "::error::Coverage ${total}% is below threshold ${threshold}%"
exit 1
fi
- name: Coverage summary
if: ${{ !cancelled() }}
run: |
if [ ! -f coverage.txt ]; then
echo "No coverage data available" >> $GITHUB_STEP_SUMMARY
exit 0
fi
total=$(go tool cover -func=coverage.txt | grep total | awk '{print $3}')
echo "## Coverage Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Total coverage: ${total}**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details><summary>Details</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
go tool cover -func=coverage.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
deadcode:
needs: fast-gate
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Fetch meta data
run: python3 scripts/fetch_meta.py
- name: Dead code check (incremental)
run: |
# Analyze current HEAD (strip line:col for stable diff across line shifts)
# Filter "go: downloading ..." lines to avoid false diffs from module cache state
go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./... 2>&1 | \
grep -v '^go: ' | \
sed 's/:[0-9][0-9]*:[0-9][0-9]*:/:/' | sort > /tmp/dc-head.txt
# Analyze base branch via worktree
git worktree add -q /tmp/dc-base "origin/${{ github.base_ref }}"
(cd /tmp/dc-base && python3 scripts/fetch_meta.py && \
go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./... 2>&1 | \
grep -v '^go: ' | \
sed 's/:[0-9][0-9]*:[0-9][0-9]*:/:/' | sort > /tmp/dc-base.txt) || {
echo "::warning::Failed to analyze base branch — skipping incremental dead code check"
git worktree remove -f /tmp/dc-base 2>/dev/null || true
exit 0
}
git worktree remove -f /tmp/dc-base
# Only new dead code blocks the PR
comm -23 /tmp/dc-head.txt /tmp/dc-base.txt > /tmp/dc-new.txt
if [ -s /tmp/dc-new.txt ]; then
echo "::group::New dead code"
cat /tmp/dc-new.txt
echo "::endgroup::"
echo "::error::New dead code detected — remove unreachable functions before merging"
exit 1
fi
echo "No new dead code introduced"
# ── Layer 3: E2E Gate ──────────────────────────────────────────────
e2e-dry-run:
needs: [unit-test, lint, script-test, deterministic-gate]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Resolve CLI E2E domains
id: e2e_domains
run: node scripts/e2e_domains.js
- name: Build lark-cli
if: ${{ steps.e2e_domains.outputs.mode != 'skip' }}
run: make build
- name: Run dry-run E2E tests
env:
LARK_CLI_BIN: ${{ github.workspace }}/lark-cli
LARKSUITE_CLI_APP_ID: dry-run
LARKSUITE_CLI_APP_SECRET: dry-run
LARKSUITE_CLI_BRAND: feishu
E2E_MODE: ${{ steps.e2e_domains.outputs.mode }}
E2E_REASON: ${{ steps.e2e_domains.outputs.reason }}
E2E_DRY_ROOT_PACKAGE: ${{ steps.e2e_domains.outputs.dry_root_package }}
E2E_DRY_PACKAGES: ${{ steps.e2e_domains.outputs.dry_packages }}
run: |
if [ "$E2E_MODE" = "skip" ]; then
echo "No dry-run CLI E2E needed: $E2E_REASON"
exit 0
fi
if [ -z "$E2E_DRY_ROOT_PACKAGE" ] && [ -z "$E2E_DRY_PACKAGES" ]; then
echo "::error::No dry-run CLI E2E packages resolved for mode $E2E_MODE"
exit 1
fi
echo "Dry-run CLI E2E domains: $E2E_MODE ($E2E_REASON)"
if [ -n "$E2E_DRY_ROOT_PACKAGE" ]; then
echo "Dry-run CLI E2E root package: $E2E_DRY_ROOT_PACKAGE"
go test -v -count=1 -timeout=5m "$E2E_DRY_ROOT_PACKAGE"
fi
if [ -n "$E2E_DRY_PACKAGES" ]; then
echo "Dry-run CLI E2E packages: $E2E_DRY_PACKAGES"
go test -v -count=1 -timeout=5m $E2E_DRY_PACKAGES -run 'DryRun|Regression'
fi
e2e-live:
needs: [unit-test, lint, script-test, deterministic-gate]
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
env:
TEST_BOT1_APP_ID: ${{ secrets.TEST_BOT1_APP_ID }}
TEST_BOT1_APP_SECRET: ${{ secrets.TEST_BOT1_APP_SECRET }}
TEST_USER_ACCESS_TOKEN: ${{ secrets.TEST_USER_ACCESS_TOKEN }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Resolve CLI E2E domains
id: e2e_domains
run: node scripts/e2e_domains.js
- name: Build lark-cli
if: ${{ steps.e2e_domains.outputs.mode != 'skip' }}
run: make build
- name: Configure bot credentials
if: ${{ steps.e2e_domains.outputs.mode != 'skip' }}
run: |
if [ -z "$TEST_BOT1_APP_ID" ] || [ -z "$TEST_BOT1_APP_SECRET" ]; then
echo "::error::Missing required secrets: TEST_BOT1_APP_ID / TEST_BOT1_APP_SECRET"
exit 1
fi
printf '%s\n' "$TEST_BOT1_APP_SECRET" | ./lark-cli config init --app-id "$TEST_BOT1_APP_ID" --app-secret-stdin
- name: Run CLI E2E tests
env:
LARK_CLI_BIN: ${{ github.workspace }}/lark-cli
E2E_MODE: ${{ steps.e2e_domains.outputs.mode }}
E2E_REASON: ${{ steps.e2e_domains.outputs.reason }}
E2E_LIVE_PACKAGES: ${{ steps.e2e_domains.outputs.live_packages }}
run: |
if [ "$E2E_MODE" = "skip" ]; then
echo "No live CLI E2E needed: $E2E_REASON"
exit 0
fi
packages="$E2E_LIVE_PACKAGES"
if [ -z "$packages" ]; then
echo "::error::No live CLI E2E packages resolved for mode $E2E_MODE"
exit 1
fi
echo "Live CLI E2E domains: $E2E_MODE ($E2E_REASON)"
echo "Live CLI E2E packages: $packages"
go run gotest.tools/gotestsum@v1.12.3 --rerun-fails=2 --rerun-fails-max-failures=20 --packages="$packages" --format testname --junitfile cli-e2e-report.xml -- -count=1 -v
- name: Publish CLI E2E test report
if: ${{ !cancelled() && steps.e2e_domains.outputs.mode != 'skip' }}
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
with:
name: CLI E2E Tests
path: cli-e2e-report.xml
reporter: java-junit
use-actions-summary: true
list-suites: all
list-tests: all
# ── Layer 4: Security & Compliance (parallel with L2-L3) ──────────
security:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Fetch meta data
run: python3 scripts/fetch_meta.py
- name: Gitleaks
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }}
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_KEY }}
- name: govulncheck
continue-on-error: true
run: go run golang.org/x/vuln/cmd/govulncheck@v1.1.4 ./...
- name: Check dependency licenses
run: go run github.com/google/go-licenses/v2@v2.0.1 check ./... --disallowed_types=forbidden,restricted,reciprocal,unknown
license-header:
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Check license headers
uses: apache/skywalking-eyes/header@8c96ee223558797cdd9eba82c0919258e1cf2dad
with:
config: .licenserc.yaml
# ── Results Gate (single required check for branch protection) ─────
results:
if: ${{ always() }}
needs: [fast-gate, unit-test, lint, script-test, deterministic-gate, coverage, deadcode, e2e-dry-run, e2e-live, security, license-header]
runs-on: ubuntu-latest
steps:
- name: Evaluate results
run: |
echo "## CI Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Layer | Job | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-----|--------|" >> $GITHUB_STEP_SUMMARY
echo "| L1 | fast-gate | ${{ needs.fast-gate.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | unit-test | ${{ needs.unit-test.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | lint | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | script-test | ${{ needs.script-test.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | deterministic-gate | ${{ needs.deterministic-gate.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | coverage | ${{ needs.coverage.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | deadcode | ${{ needs.deadcode.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L3 | e2e-dry-run | ${{ needs.e2e-dry-run.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L3 | e2e-live | ${{ needs.e2e-live.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L4 | security | ${{ needs.security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L4 | license-header | ${{ needs.license-header.result }} |" >> $GITHUB_STEP_SUMMARY
# Any failure or cancellation in any job blocks the merge.
# Legitimately skipped jobs (deadcode on push, e2e-live on fork,
# license-header on push) are OK.
FAILED=0
for result in \
"${{ needs.fast-gate.result }}" \
"${{ needs.unit-test.result }}" \
"${{ needs.lint.result }}" \
"${{ needs.script-test.result }}" \
"${{ needs.deterministic-gate.result }}" \
"${{ needs.coverage.result }}" \
"${{ needs.deadcode.result }}" \
"${{ needs.e2e-dry-run.result }}" \
"${{ needs.e2e-live.result }}" \
"${{ needs.security.result }}" \
"${{ needs.license-header.result }}"; do
if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then
FAILED=1
fi
done
if [ "$FAILED" = "1" ]; then
echo ""
echo "::error::One or more CI jobs failed — see table above"
exit 1
fi
+28
View File
@@ -0,0 +1,28 @@
name: Comment Audit
on:
issue_comment:
types: [created, edited]
pull_request_review:
types: [submitted, edited]
pull_request_review_comment:
types: [created, edited]
permissions:
contents: read
jobs:
public-content-comment-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- name: Post-publication comment audit
run: |
mkdir -p .tmp/comment-audit
cp "$GITHUB_EVENT_PATH" .tmp/comment-audit/event.json
go run ./internal/qualitygate/cmd/comment-audit --event .tmp/comment-audit/event.json --kind "$GITHUB_EVENT_NAME"
+57
View File
@@ -0,0 +1,57 @@
name: Issue Labels
on:
schedule:
- cron: '0 * * * *' # every hour
workflow_dispatch:
inputs:
dry_run:
description: "Do not write labels, only print planned changes"
required: false
default: true
type: boolean
permissions:
contents: read
issues: write
concurrency:
group: issue-labels
cancel-in-progress: true
jobs:
sync-issue-labels:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# v6+ uses Node 24 runtime for JavaScript actions.
- uses: actions/setup-node@v6
with:
node-version: '24'
- name: Sync managed issue labels
id: sync_issue_labels
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
run: |
args=(
"--max-issues" "300"
)
# Schedule runs should write labels by default.
# Manual runs default to dry-run unless explicitly disabled.
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${INPUT_DRY_RUN:-true}" = "true" ]; then
args+=("--dry-run" "--json")
fi
node scripts/issue-labels/index.js "${args[@]}"
- name: Warn when label sync fails
if: ${{ always() && steps.sync_issue_labels.outcome == 'failure' }}
run: |
echo "::warning::Issue label sync failed; labels may be stale."
echo "⚠️ Issue label sync failed; labels may be stale." >> "$GITHUB_STEP_SUMMARY"
+149
View File
@@ -0,0 +1,149 @@
name: PR Preview Package Comment
on:
workflow_run:
workflows: ["PR Preview Package"]
types: [completed]
permissions:
actions: read
contents: read
issues: write
pull-requests: write
jobs:
comment:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Check comment payload artifact
id: payload
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const runId = context.payload.workflow_run?.id;
const { data } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId,
per_page: 100,
});
const found = Boolean(
data.artifacts?.some((artifact) => artifact.name === "pkg-pr-new-comment-payload")
);
core.setOutput("found", found ? "true" : "false");
if (!found) {
core.notice("No comment payload artifact found for this run; skipping comment.");
}
- name: Download comment payload
if: steps.payload.outputs.found == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: pkg-pr-new-comment-payload
repository: ${{ github.repository }}
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Comment install command
if: steps.payload.outputs.found == 'true'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const fs = require("fs");
const payload = JSON.parse(fs.readFileSync("pkg-pr-new-comment-payload.json", "utf8"));
const url = payload?.url;
const payloadPr = payload?.pr;
const sourceRepo = payload?.sourceRepo;
const sourceBranch = payload?.sourceBranch;
if (!Number.isInteger(payloadPr)) {
throw new Error(`Invalid PR number in artifact payload: ${payloadPr}`);
}
if (payloadPr <= 0) {
throw new Error(`Invalid PR number in artifact payload: ${payloadPr}`);
}
const issueNumber = payloadPr;
const runPrNumber = context.payload.workflow_run?.pull_requests?.[0]?.number;
if (Number.isInteger(runPrNumber) && runPrNumber !== issueNumber) {
throw new Error(
`PR number mismatch between workflow_run (${runPrNumber}) and artifact payload (${issueNumber})`,
);
}
if (typeof url !== "string" || url.trim() !== url || /[\u0000-\u001F\u007F]/.test(url)) {
throw new Error(`Invalid package URL in payload: ${url}`);
}
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch {
throw new Error(`Invalid package URL in payload: ${url}`);
}
if (parsedUrl.protocol !== "https:" || parsedUrl.hostname !== "pkg.pr.new") {
throw new Error(`Invalid package URL in payload: ${url}`);
}
const safeRepoPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
const safeBranchPattern = /^[A-Za-z0-9._\/-]+$/;
const hasSkillSource =
typeof sourceRepo === "string" &&
typeof sourceBranch === "string" &&
safeRepoPattern.test(sourceRepo) &&
safeBranchPattern.test(sourceBranch);
const skillSection = hasSkillSource
? [
"",
"### 🧩 Skill update",
"",
"```bash",
`npx skills add ${sourceRepo}#${sourceBranch} -y -g`,
"```",
]
: [
"",
"### 🧩 Skill update",
"",
"_Unavailable for this PR because source repo/branch metadata is missing._",
];
const body = [
"<!-- pkg-pr-new-install-guide -->",
"## 🚀 PR Preview Install Guide",
"",
"### 🧰 CLI update",
"",
"```bash",
`npm i -g ${url}`,
"```",
...skillSection,
].join("\n");
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100,
});
const existing = comments.find((comment) =>
comment.user?.login === "github-actions[bot]" &&
typeof comment.body === "string" &&
comment.body.includes("<!-- pkg-pr-new-install-guide -->")
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body,
});
}
+71
View File
@@ -0,0 +1,71 @@
name: PR Preview Package
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [main]
permissions:
contents: read
jobs:
publish:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
with:
node-version: lts/*
- name: Build preview package
run: ./scripts/build-pkg-pr-new.sh
- name: Publish to pkg.pr.new
run: npx pkg-pr-new publish --no-compact --json output.json --comment=off ./.pkg-pr-new
- name: Build comment payload
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
SOURCE_REPO: ${{ github.event.pull_request.head.repo.full_name }}
SOURCE_BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
node <<'NODE'
const fs = require("fs");
const output = JSON.parse(fs.readFileSync("output.json", "utf8"));
const url = output?.packages?.[0]?.url;
if (!url) throw new Error("No package URL found in output.json");
if (!url.startsWith("https://pkg.pr.new/")) {
throw new Error(`Unexpected package URL: ${url}`);
}
const pr = Number(process.env.PR_NUMBER);
if (!Number.isInteger(pr) || pr <= 0) {
throw new Error(`Invalid PR_NUMBER: ${process.env.PR_NUMBER}`);
}
const payload = {
pr,
url,
sourceRepo: process.env.SOURCE_REPO || "",
sourceBranch: process.env.SOURCE_BRANCH || "",
};
fs.writeFileSync(
"pkg-pr-new-comment-payload.json",
JSON.stringify(payload),
);
NODE
- name: Upload comment payload
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pkg-pr-new-comment-payload
path: pkg-pr-new-comment-payload.json
+31
View File
@@ -0,0 +1,31 @@
name: Test PR Label Logic
on:
push:
branches: [main]
paths:
- "scripts/pr-labels/**"
- ".github/workflows/pr-labels-test.yml"
pull_request:
branches: [main]
paths:
- "scripts/pr-labels/**"
- ".github/workflows/pr-labels-test.yml"
permissions:
contents: read
jobs:
test-pr-labels:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Run PR label regression tests
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/pr-labels/test.js
+43
View File
@@ -0,0 +1,43 @@
name: PR Labels
on:
pull_request_target:
# NOTE: This event runs with base-branch code and write permissions.
# Do NOT add `ref: github.event.pull_request.head.sha` to the checkout step,
# as that would execute untrusted PR code with elevated access.
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
permissions:
contents: read
pull-requests: write
issues: write
jobs:
sync-pr-labels:
if: ${{ github.event.pull_request.state == 'open' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Sync managed PR labels
id: sync_pr_labels
# Labeling is best-effort and must not block PR merges.
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/pr-labels/index.js
- name: Warn when label sync fails
if: ${{ always() && steps.sync_pr_labels.outcome == 'failure' }}
run: |
echo "::warning::PR label sync failed; labels may be stale."
echo "⚠️ PR label sync failed; labels may be stale." >> "$GITHUB_STEP_SUMMARY"
+60
View File
@@ -0,0 +1,60 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: read
jobs:
goreleaser:
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.23'
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.x'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6
with:
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-npm:
needs: goreleaser
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Download checksums from release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${GITHUB_REF_NAME}"
gh release download "${TAG}" --pattern checksums.txt --dir .
test -s checksums.txt || { echo "checksums.txt missing or empty for ${TAG}"; exit 1; }
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public
+611
View File
@@ -0,0 +1,611 @@
name: Semantic Review
on:
workflow_run:
workflows: ["CI"]
types: [completed]
permissions:
actions: read
contents: read
jobs:
pr-quality-summary:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
issues: write
pull-requests: write
steps:
- name: Verify workflow run and pull request for summary
id: pr
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const run = context.payload.workflow_run;
if (run.name !== "CI") throw new Error(`unexpected workflow name: ${run.name}`);
let workflowPath = run.path || "";
if (!workflowPath) {
const workflowId = Number(run.workflow_id || 0);
if (!Number.isInteger(workflowId) || workflowId <= 0) throw new Error("missing workflow id");
const { data: workflow } = await github.rest.actions.getWorkflow({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: workflowId,
});
workflowPath = workflow.path || "";
}
if (workflowPath !== ".github/workflows/ci.yml") throw new Error(`unexpected workflow path: ${workflowPath}`);
if (run.event !== "pull_request") throw new Error(`unexpected event: ${run.event}`);
if (run.repository.id !== context.payload.repository.id) throw new Error("repository id mismatch");
if (run.repository.full_name !== context.payload.repository.full_name) throw new Error("repository name mismatch");
if (typeof run.head_sha !== "string" || run.head_sha.length !== 40) throw new Error("invalid head sha");
const runPRs = Array.isArray(run.pull_requests) ? run.pull_requests : [];
if (runPRs.length > 1) {
throw new Error(`ambiguous workflow_run pull request bindings: ${runPRs.length}`);
}
let prNumber = Number(runPRs[0]?.number || 0);
const eventBaseSha = runPRs[0]?.base?.sha || "";
const eventHeadSha = runPRs[0]?.head?.sha || "";
const targetHeadSha = run.head_sha;
if (!/^[a-f0-9]{40}$/i.test(targetHeadSha)) throw new Error("invalid PR head sha");
if (eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
core.notice("PR quality summary using workflow_run head_sha because workflow_run pull request head differs from the CI run head");
}
const factsArtifactPattern = /^quality-gate-facts-([a-f0-9]{40})-([a-f0-9]{40})$/i;
const { data: artifactData } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run.id,
per_page: 100,
});
const factsArtifacts = artifactData.artifacts.filter((artifact) => factsArtifactPattern.test(artifact.name));
let factsArtifactName = "";
let artifactBaseSha = "";
let artifactError = "";
if (factsArtifacts.length !== 1) {
artifactError = `expected exactly one base-bound quality gate facts artifact, got ${factsArtifacts.length}`;
} else {
factsArtifactName = factsArtifacts[0].name;
const [, parsedBaseSha, artifactHeadSha] = factsArtifactName.match(factsArtifactPattern);
if (artifactHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
artifactError = "facts artifact head sha does not match verified PR head sha";
factsArtifactName = "";
} else {
artifactBaseSha = parsedBaseSha;
if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) {
core.notice("PR quality summary using facts artifact base because workflow_run pull request base differs from the CI facts artifact base");
}
}
}
if (!prNumber) {
const { data: associatedPRs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: targetHeadSha,
});
const candidatePRs = associatedPRs.filter((candidate) =>
candidate.base?.repo?.id === context.payload.repository.id &&
candidate.head?.sha === targetHeadSha
);
const openCandidatePRs = candidatePRs.filter((candidate) => candidate.state === "open");
if (openCandidatePRs.length > 1) {
throw new Error(`ambiguous open PRs for workflow_run head ${targetHeadSha}: ${openCandidatePRs.length}`);
}
if (openCandidatePRs.length === 1) {
prNumber = openCandidatePRs[0].number;
} else if (candidatePRs.length > 0) {
core.notice("PR quality summary skipped: workflow_run target PR is no longer open");
core.setOutput("stale", "true");
return;
}
}
if (!prNumber) {
const candidatePRs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: "all",
per_page: 100,
}).then((prs) => prs.filter((candidate) =>
candidate.base?.repo?.id === context.payload.repository.id &&
candidate.head?.sha === targetHeadSha
));
const openCandidatePRs = candidatePRs.filter((candidate) => candidate.state === "open");
if (openCandidatePRs.length > 1) {
throw new Error(`ambiguous open PRs from pull list fallback for workflow_run head ${targetHeadSha}: ${openCandidatePRs.length}`);
}
if (openCandidatePRs.length === 1) {
prNumber = openCandidatePRs[0].number;
} else if (candidatePRs.length > 0) {
core.notice("PR quality summary skipped: workflow_run target PR is no longer open");
core.setOutput("stale", "true");
return;
} else {
throw new Error(`expected one open PR from pull list fallback for workflow_run head ${targetHeadSha}, got ${candidatePRs.length}`);
}
}
if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("missing pull request binding");
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
if (pr.base.repo.id !== context.payload.repository.id) throw new Error("PR base repo mismatch");
if (pr.state !== "open") {
core.notice("PR quality summary skipped: workflow_run target PR is no longer open");
core.setOutput("stale", "true");
return;
}
if (pr.head.sha !== targetHeadSha) {
core.notice("PR quality summary skipped: workflow_run is stale for this PR head");
core.setOutput("stale", "true");
return;
}
const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha;
if (!/^[a-f0-9]{40}$/i.test(baseSha)) throw new Error("invalid PR base sha");
if ((eventBaseSha || artifactBaseSha) && pr.base.sha !== baseSha) {
core.notice("PR quality summary skipped: workflow_run is stale for this PR base");
core.setOutput("stale", "true");
return;
}
if (artifactError) {
core.warning(`quality gate facts artifact binding is unavailable: ${artifactError}`);
}
core.setOutput("pr_number", String(prNumber));
core.setOutput("head_sha", targetHeadSha);
core.setOutput("base_sha", baseSha);
core.setOutput("run_id", String(run.id));
core.setOutput("facts_artifact_name", factsArtifactName);
core.setOutput("artifact_error", artifactError);
core.setOutput("stale", "false");
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
id: checkout
if: ${{ steps.pr.outputs.stale != 'true' }}
with:
ref: ${{ steps.pr.outputs.base_sha }}
persist-credentials: false
- name: Verify summary facts artifact metadata
id: artifact
if: ${{ steps.pr.outputs.stale != 'true' && steps.pr.outputs.facts_artifact_name != '' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const run = context.payload.workflow_run;
const factsArtifactName = "${{ steps.pr.outputs.facts_artifact_name }}";
const { data } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run.id,
per_page: 100,
});
const artifacts = data.artifacts.filter(a => a.name === factsArtifactName);
if (artifacts.length !== 1) throw new Error(`expected exactly one quality-gate-facts artifact, got ${artifacts.length}`);
const artifact = artifacts[0];
if (artifact.expired) throw new Error("quality-gate-facts artifact expired");
if (artifact.size_in_bytes <= 0 || artifact.size_in_bytes > 5 * 1024 * 1024) {
throw new Error(`invalid artifact size: ${artifact.size_in_bytes}`);
}
if (!artifact.digest) throw new Error("facts artifact digest is missing from GitHub API response");
core.setOutput("artifact_id", String(artifact.id));
core.setOutput("artifact_digest", artifact.digest);
- name: Download facts artifact zip
if: ${{ steps.pr.outputs.stale != 'true' && steps.artifact.outputs.artifact_id != '' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
id: download
with:
script: |
const fs = require("fs");
const path = require("path");
const artifactId = Number("${{ steps.artifact.outputs.artifact_id }}");
if (!Number.isInteger(artifactId) || artifactId <= 0) throw new Error("invalid artifact id");
const { data } = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifactId,
archive_format: "zip",
});
const zipPath = path.join(process.env.RUNNER_TEMP, "quality-gate-facts.zip");
fs.writeFileSync(zipPath, Buffer.from(data));
core.setOutput("zip_path", zipPath);
- name: Verify and extract summary facts artifact
if: ${{ steps.pr.outputs.stale != 'true' && steps.download.outputs.zip_path != '' }}
env:
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
SEMANTIC_REVIEW_DECISION_OUT: decision.json
SEMANTIC_REVIEW_MARKDOWN_OUT: semantic-review.md
run: node scripts/semantic-review-verify-artifact.js '${{ steps.download.outputs.zip_path }}' facts.json '${{ steps.artifact.outputs.artifact_digest }}'
- name: Publish PR quality summary
if: ${{ always() && steps.pr.outputs.stale != 'true' && steps.checkout.outcome == 'success' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
CI_QUALITY_SUMMARY_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
CI_QUALITY_SUMMARY_BASE_SHA: ${{ steps.pr.outputs.base_sha }}
CI_QUALITY_SUMMARY_PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
CI_QUALITY_SUMMARY_RUN_ID: ${{ steps.pr.outputs.run_id }}
CI_QUALITY_SUMMARY_ARTIFACT_ERROR: ${{ steps.pr.outputs.artifact_error }}
with:
script: |
const { publish } = require("./scripts/ci-quality-summary-publish.js");
await publish({ github, context, core });
semantic-review:
needs: pr-quality-summary
if: always() && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
permissions:
actions: read
checks: write
contents: read
issues: write
pull-requests: write
steps:
- name: Verify workflow run and pull request
id: pr
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const run = context.payload.workflow_run;
if (run.name !== "CI") throw new Error(`unexpected workflow name: ${run.name}`);
let workflowPath = run.path || "";
if (!workflowPath) {
const workflowId = Number(run.workflow_id || 0);
if (!Number.isInteger(workflowId) || workflowId <= 0) throw new Error("missing workflow id");
const { data: workflow } = await github.rest.actions.getWorkflow({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: workflowId,
});
workflowPath = workflow.path || "";
}
if (workflowPath !== ".github/workflows/ci.yml") throw new Error(`unexpected workflow path: ${workflowPath}`);
if (run.event !== "pull_request") throw new Error(`unexpected event: ${run.event}`);
if (run.conclusion !== "success") throw new Error(`unexpected conclusion: ${run.conclusion}`);
if (run.repository.id !== context.payload.repository.id) throw new Error("repository id mismatch");
if (run.repository.full_name !== context.payload.repository.full_name) throw new Error("repository name mismatch");
if (typeof run.head_sha !== "string" || run.head_sha.length !== 40) throw new Error("invalid head sha");
const runPRs = Array.isArray(run.pull_requests) ? run.pull_requests : [];
if (runPRs.length > 1) {
throw new Error(`ambiguous workflow_run pull request bindings: ${runPRs.length}`);
}
let prNumber = Number(runPRs[0]?.number || 0);
const eventBaseSha = runPRs[0]?.base?.sha || "";
const eventHeadSha = runPRs[0]?.head?.sha || "";
const targetHeadSha = run.head_sha;
if (!/^[a-f0-9]{40}$/i.test(targetHeadSha)) throw new Error("invalid PR head sha");
if (eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
core.notice("semantic review using workflow_run head_sha because workflow_run pull request head differs from the CI run head");
}
const factsArtifactPattern = /^quality-gate-facts-([a-f0-9]{40})-([a-f0-9]{40})$/i;
const { data: artifactData } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run.id,
per_page: 100,
});
const factsArtifacts = artifactData.artifacts.filter((artifact) => factsArtifactPattern.test(artifact.name));
let factsArtifactName = "";
let artifactBaseSha = "";
let artifactError = "";
if (factsArtifacts.length !== 1) {
artifactError = `expected exactly one base-bound quality gate facts artifact, got ${factsArtifacts.length}`;
} else {
factsArtifactName = factsArtifacts[0].name;
const [, parsedBaseSha, artifactHeadSha] = factsArtifactName.match(factsArtifactPattern);
if (artifactHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
artifactError = "facts artifact head sha does not match verified PR head sha";
factsArtifactName = "";
} else {
artifactBaseSha = parsedBaseSha;
if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) {
core.notice("semantic review using facts artifact base because workflow_run pull request base differs from the CI facts artifact base");
}
}
}
if (!prNumber) {
const { data: associatedPRs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: targetHeadSha,
});
const candidatePRs = associatedPRs.filter((candidate) =>
candidate.base?.repo?.id === context.payload.repository.id &&
candidate.head?.sha === targetHeadSha
);
const openCandidatePRs = candidatePRs.filter((candidate) => candidate.state === "open");
if (openCandidatePRs.length > 1) {
throw new Error(`ambiguous open PRs for workflow_run head ${targetHeadSha}: ${openCandidatePRs.length}`);
}
if (openCandidatePRs.length === 1) {
prNumber = openCandidatePRs[0].number;
} else if (candidatePRs.length > 0) {
core.notice("semantic review skipped: workflow_run target PR is no longer open");
core.setOutput("stale", "true");
return;
}
}
if (!prNumber) {
const candidatePRs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: "all",
per_page: 100,
}).then((prs) => prs.filter((candidate) =>
candidate.base?.repo?.id === context.payload.repository.id &&
candidate.head?.sha === targetHeadSha
));
const openCandidatePRs = candidatePRs.filter((candidate) => candidate.state === "open");
if (openCandidatePRs.length > 1) {
throw new Error(`ambiguous open PRs from pull list fallback for workflow_run head ${targetHeadSha}: ${openCandidatePRs.length}`);
}
if (openCandidatePRs.length === 1) {
prNumber = openCandidatePRs[0].number;
} else if (candidatePRs.length > 0) {
core.notice("semantic review skipped: workflow_run target PR is no longer open");
core.setOutput("stale", "true");
return;
} else {
throw new Error(`expected one open PR from pull list fallback for workflow_run head ${targetHeadSha}, got ${candidatePRs.length}`);
}
}
if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("missing pull request binding");
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
if (pr.base.repo.id !== context.payload.repository.id) throw new Error("PR base repo mismatch");
if (pr.state !== "open") {
core.notice("semantic review skipped: workflow_run target PR is no longer open");
core.setOutput("stale", "true");
return;
}
if (!pr.head.repo) {
core.notice("semantic review skipped: workflow_run target PR head repository is unavailable");
core.setOutput("stale", "true");
return;
}
if (pr.head.sha !== targetHeadSha) {
core.notice("semantic review skipped: workflow_run is stale for this PR head");
core.setOutput("stale", "true");
return;
}
const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha;
if (!/^[a-f0-9]{40}$/i.test(baseSha)) throw new Error("invalid PR base sha");
if ((eventBaseSha || artifactBaseSha) && pr.base.sha !== baseSha) {
core.notice("semantic review skipped: workflow_run is stale for this PR base");
core.setOutput("stale", "true");
return;
}
if (artifactError) {
core.warning(`semantic review facts artifact binding is unavailable: ${artifactError}`);
}
core.setOutput("pr_number", String(prNumber));
core.setOutput("head_sha", targetHeadSha);
core.setOutput("base_sha", baseSha);
core.setOutput("head_owner", pr.head.repo.owner.login);
core.setOutput("head_repo", pr.head.repo.name);
core.setOutput("head_repo_id", String(pr.head.repo.id));
core.setOutput("head_is_base_repo", pr.head.repo.id === context.payload.repository.id ? "true" : "false");
core.setOutput("run_id", String(run.id));
core.setOutput("facts_artifact_name", factsArtifactName);
core.setOutput("artifact_error", artifactError);
core.setOutput("stale", "false");
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
id: checkout
if: ${{ steps.pr.outputs.stale != 'true' }}
with:
ref: ${{ steps.pr.outputs.base_sha }}
persist-credentials: false
- name: Publish pre-checkout semantic review failure
if: ${{ failure() && steps.pr.outputs.stale != 'true' && steps.checkout.outcome != 'success' && steps.pr.outputs.head_sha != '' && steps.pr.outputs.pr_number != '' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
SEMANTIC_REVIEW_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
SEMANTIC_REVIEW_BASE_SHA: ${{ steps.pr.outputs.base_sha }}
SEMANTIC_REVIEW_PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
SEMANTIC_REVIEW_RUN_ID: ${{ steps.pr.outputs.run_id }}
with:
script: |
const runtimeBlockMode = process.env.SEMANTIC_REVIEW_BLOCK === "true";
const pr = Number(process.env.SEMANTIC_REVIEW_PR_NUMBER || 0);
const headSha = process.env.SEMANTIC_REVIEW_HEAD_SHA || "";
const baseSha = process.env.SEMANTIC_REVIEW_BASE_SHA || "";
if (!Number.isInteger(pr) || pr <= 0 || !/^[a-f0-9]{40}$/i.test(headSha) || !/^[a-f0-9]{40}$/i.test(baseSha)) {
throw new Error("missing verified semantic review target");
}
const { data: pull } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr,
});
if (pull.state !== "open") {
core.notice("semantic review skipped infrastructure failure check: PR is no longer open");
return;
}
if (pull.head.sha !== headSha) {
core.notice("semantic review skipped infrastructure failure check: PR head changed");
return;
}
if (pull.base.sha !== baseSha) {
core.notice("semantic review skipped infrastructure failure check: PR base changed");
return;
}
if (pull.base.repo.id !== context.payload.repository.id) {
throw new Error("PR base repo mismatch before infrastructure failure check");
}
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: runtimeBlockMode ? "semantic-review/result" : "semantic-review/observe",
head_sha: headSha,
status: "completed",
conclusion: runtimeBlockMode ? "failure" : "neutral",
output: {
title: "Semantic review infrastructure failure",
summary: "Semantic review could not checkout the verified base commit. Inspect the workflow logs before relying on semantic review output.",
},
});
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
if: ${{ steps.pr.outputs.stale != 'true' }}
with:
go-version-file: go.mod
- name: Verify semantic facts artifact metadata
id: artifact
if: ${{ steps.pr.outputs.stale != 'true' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const run = context.payload.workflow_run;
const factsArtifactName = "${{ steps.pr.outputs.facts_artifact_name }}";
if (!/^quality-gate-facts-[a-f0-9]{40}-[a-f0-9]{40}$/i.test(factsArtifactName)) {
throw new Error("missing verified facts artifact binding");
}
const { data } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run.id,
per_page: 100,
});
const artifacts = data.artifacts.filter(a => a.name === factsArtifactName);
if (artifacts.length !== 1) throw new Error(`expected exactly one quality-gate-facts artifact, got ${artifacts.length}`);
const artifact = artifacts[0];
if (artifact.expired) throw new Error("quality-gate-facts artifact expired");
if (artifact.size_in_bytes <= 0 || artifact.size_in_bytes > 5 * 1024 * 1024) {
throw new Error(`invalid artifact size: ${artifact.size_in_bytes}`);
}
if (!artifact.digest) throw new Error("facts artifact digest is missing from GitHub API response");
core.setOutput("artifact_id", String(artifact.id));
core.setOutput("artifact_digest", artifact.digest);
- name: Download facts artifact zip
if: ${{ steps.pr.outputs.stale != 'true' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
id: download
with:
script: |
const fs = require("fs");
const path = require("path");
const artifactId = Number("${{ steps.artifact.outputs.artifact_id }}");
if (!Number.isInteger(artifactId) || artifactId <= 0) throw new Error("invalid artifact id");
const { data } = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifactId,
archive_format: "zip",
});
const zipPath = path.join(process.env.RUNNER_TEMP, "quality-gate-facts.zip");
fs.writeFileSync(zipPath, Buffer.from(data));
core.setOutput("zip_path", zipPath);
- name: Verify and extract semantic facts artifact
if: ${{ steps.pr.outputs.stale != 'true' }}
env:
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
SEMANTIC_REVIEW_DECISION_OUT: decision.json
SEMANTIC_REVIEW_MARKDOWN_OUT: semantic-review.md
run: node scripts/semantic-review-verify-artifact.js '${{ steps.download.outputs.zip_path }}' facts.json '${{ steps.artifact.outputs.artifact_digest }}'
- name: Download PR semantic waiver config
id: waiver_config
if: ${{ steps.pr.outputs.stale != 'true' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
SEMANTIC_REVIEW_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
SEMANTIC_REVIEW_HEAD_OWNER: ${{ steps.pr.outputs.head_owner }}
SEMANTIC_REVIEW_HEAD_REPO: ${{ steps.pr.outputs.head_repo }}
SEMANTIC_REVIEW_HEAD_IS_BASE_REPO: ${{ steps.pr.outputs.head_is_base_repo }}
with:
script: |
const fs = require("fs");
const path = require("path");
const headSha = process.env.SEMANTIC_REVIEW_HEAD_SHA || "";
if (!/^[a-f0-9]{40}$/i.test(headSha)) {
throw new Error("missing verified semantic review target");
}
const headOwner = process.env.SEMANTIC_REVIEW_HEAD_OWNER || "";
const headRepo = process.env.SEMANTIC_REVIEW_HEAD_REPO || "";
if (!headOwner || !headRepo) {
throw new Error("missing verified semantic review head repository");
}
const waiverPath = "internal/qualitygate/config/semantic/waivers.txt";
const outPath = path.join(process.env.RUNNER_TEMP, "semantic-review-waivers.txt");
const headIsBaseRepo = process.env.SEMANTIC_REVIEW_HEAD_IS_BASE_REPO === "true";
if (!headIsBaseRepo) {
core.notice("fork PR semantic waiver config is ignored");
core.setOutput("path", "");
return;
}
let content = "";
try {
const { data } = await github.rest.repos.getContent({
owner: headOwner,
repo: headRepo,
path: waiverPath,
ref: headSha,
});
if (Array.isArray(data) || data.type !== "file" || data.encoding !== "base64") {
throw new Error(`${waiverPath} is not a base64 file at PR head`);
}
if (data.size > 256 * 1024) {
throw new Error(`${waiverPath} is too large: ${data.size} bytes`);
}
content = Buffer.from(data.content, "base64").toString("utf8");
} catch (err) {
if (err.status !== 404) {
throw err;
}
}
fs.writeFileSync(outPath, content);
core.setOutput("path", outPath);
- name: Run semantic review
id: semantic
if: ${{ steps.pr.outputs.stale != 'true' }}
env:
ARK_API_KEY: ${{ secrets.ARK_API_KEY }}
ARK_BASE_URL: ${{ vars.ARK_BASE_URL }}
ARK_MODEL: ${{ vars.ARK_MODEL }}
ARK_TIMEOUT_SECONDS: ${{ vars.ARK_TIMEOUT_SECONDS }}
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
run: |
args=(
--repo .
--facts facts.json
--decision-out decision.json
--markdown-out semantic-review.md
)
if [ -n "${{ steps.waiver_config.outputs.path }}" ]; then
args+=(--waivers-file '${{ steps.waiver_config.outputs.path }}')
fi
if [ "$SEMANTIC_REVIEW_BLOCK" = "true" ]; then
args+=(--block)
fi
go run ./internal/qualitygate/cmd/semantic-review "${args[@]}"
- name: Publish semantic review
if: ${{ always() && steps.pr.outputs.stale != 'true' && steps.checkout.outcome == 'success' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
SEMANTIC_REVIEW_BLOCK: ${{ vars.SEMANTIC_REVIEW_BLOCK }}
SEMANTIC_REVIEW_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
SEMANTIC_REVIEW_BASE_SHA: ${{ steps.pr.outputs.base_sha }}
SEMANTIC_REVIEW_PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
SEMANTIC_REVIEW_RUN_ID: ${{ steps.pr.outputs.run_id }}
with:
script: |
const { publish } = require("./scripts/semantic-review-publish.js");
await publish({ github, context, core });
+32
View File
@@ -0,0 +1,32 @@
name: Skill Format Check
on:
push:
branches: [main]
paths:
- "skills/**"
- "scripts/skill-format-check/**"
- ".github/workflows/skill-format-check.yml"
pull_request:
branches: [main]
paths:
- "skills/**"
- "scripts/skill-format-check/**"
- ".github/workflows/skill-format-check.yml"
permissions:
contents: read
jobs:
check-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Run Skill Format Check
run: node scripts/skill-format-check/index.js