Skip to content

Commit 04a6110

Browse files
authored
ci: add CI, release, and security workflows (#2)
1 parent 774261e commit 04a6110

9 files changed

Lines changed: 284 additions & 1 deletion

File tree

.github/labeler.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# .github/labeler.yml — path-based PR auto-labels (actions/labeler@v5 format).
2+
documentation:
3+
- changed-files:
4+
- any-glob-to-any-file: ["**/*.md", "docs/**"]
5+
ci:
6+
- changed-files:
7+
- any-glob-to-any-file: [".github/**"]
8+
dependencies:
9+
- changed-files:
10+
- any-glob-to-any-file:
11+
["**/package.json", "**/pubspec.yaml", "**/go.mod", "**/*.lock", "**/*lock.yaml"]
12+
tests:
13+
- changed-files:
14+
- any-glob-to-any-file: ["**/*.test.*", "**/*_test.*", "test/**", "**/__tests__/**"]

.github/workflows/ci.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["master"]
6+
pull_request:
7+
branches: ["master"]
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ci-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
build:
18+
name: build
19+
runs-on: ubuntu-latest
20+
# Dummy build-time env so steps like `next build` that read env at build
21+
# succeed in CI (same approach as portfolio-nextjs). Add this repo's
22+
# build-time vars here with placeholder values.
23+
env:
24+
CI: "true"
25+
NEXT_PUBLIC_GITHUB_TOKEN: dummy-token-for-build
26+
steps:
27+
- uses: actions/checkout@v6
28+
with:
29+
persist-credentials: false
30+
31+
# Detect the package manager from the lockfile so one workflow serves
32+
# pnpm / npm / yarn / bun repos.
33+
- name: Detect package manager
34+
id: pm
35+
run: |
36+
if [ -f bun.lockb ] || [ -f bun.lock ]; then echo "pm=bun" >> "$GITHUB_OUTPUT"
37+
elif [ -f pnpm-lock.yaml ]; then echo "pm=pnpm" >> "$GITHUB_OUTPUT"
38+
elif [ -f yarn.lock ]; then echo "pm=yarn" >> "$GITHUB_OUTPUT"
39+
else echo "pm=npm" >> "$GITHUB_OUTPUT"; fi
40+
41+
- if: steps.pm.outputs.pm == 'pnpm'
42+
uses: pnpm/action-setup@v4
43+
with:
44+
version: 9
45+
- if: steps.pm.outputs.pm == 'bun'
46+
uses: oven-sh/setup-bun@v2
47+
- if: steps.pm.outputs.pm != 'bun'
48+
uses: actions/setup-node@v4
49+
with:
50+
node-version: lts/*
51+
cache: ${{ steps.pm.outputs.pm }}
52+
53+
- name: Install
54+
run: |
55+
case "${{ steps.pm.outputs.pm }}" in
56+
bun) bun install --frozen-lockfile ;;
57+
pnpm) pnpm install --frozen-lockfile ;;
58+
yarn) yarn install --frozen-lockfile ;;
59+
npm) npm ci ;;
60+
esac
61+
62+
# Run a script only if package.json defines it, so repos missing lint/
63+
# build/test don't fail the job. `run_if` echoes then runs via the pm.
64+
- name: Run available scripts (lint, ts:check, build, test)
65+
env:
66+
PM: ${{ steps.pm.outputs.pm }}
67+
run: |
68+
has() { jq -e --arg s "$1" '.scripts[$s] // empty' package.json >/dev/null 2>&1; }
69+
run() {
70+
case "$PM" in
71+
bun) bun run "$1" ;;
72+
*) "$PM" run "$1" ;;
73+
esac
74+
}
75+
for script in lint ts:check typecheck build test; do
76+
if has "$script"; then
77+
echo "::group::$script"; run "$script"; echo "::endgroup::"
78+
else
79+
echo "skip: no \"$script\" script"
80+
fi
81+
done
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Dependency Review
2+
3+
# Flags vulnerable or disallowed-license dependencies introduced by a PR before
4+
# they merge. Replaces Dependabot's alerting with a hard PR gate.
5+
on:
6+
pull_request:
7+
branches: ["master"]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
dependency-review:
14+
name: Dependency review
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v6
18+
with:
19+
persist-credentials: false
20+
- uses: actions/dependency-review-action@v4
21+
with:
22+
fail-on-severity: high
23+
comment-summary-in-pr: on-failure

.github/workflows/labeler.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Labeler
2+
3+
# Auto-labels PRs by the paths they touch (config in .github/labeler.yml).
4+
on:
5+
pull_request_target:
6+
types: [opened, synchronize, reopened]
7+
8+
permissions:
9+
contents: read
10+
pull-requests: write
11+
12+
jobs:
13+
label:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/labeler@v5
17+
with:
18+
sync-labels: true

.github/workflows/release.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Release
2+
3+
# On push to the default branch (a merged PR), tag v<package.json version> and
4+
# publish a GitHub Release with generated notes — unless the tag already exists.
5+
on:
6+
push:
7+
branches: ["master"]
8+
workflow_dispatch: {}
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
release:
15+
name: Tag and release
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: write
19+
steps:
20+
- uses: actions/checkout@v6
21+
with:
22+
fetch-depth: 0
23+
persist-credentials: false
24+
- name: Read version
25+
id: v
26+
run: |
27+
version=$(jq -r .version package.json)
28+
echo "tag=v$version" >> "$GITHUB_OUTPUT"
29+
- name: Release if new
30+
env:
31+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32+
TAG: ${{ steps.v.outputs.tag }}
33+
run: |
34+
if gh release view "$TAG" >/dev/null 2>&1 \
35+
|| git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then
36+
echo "Tag $TAG already exists — skipping."
37+
exit 0
38+
fi
39+
gh release create "$TAG" --target "${{ github.sha }}" --title "$TAG" --generate-notes
40+
echo "Released $TAG ✓"

.github/workflows/scorecard.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Scorecard
2+
3+
# OpenSSF Scorecard — supply-chain security posture. Runs on push to the default
4+
# branch and weekly; uploads SARIF so findings show in the Security tab.
5+
on:
6+
push:
7+
branches: ["master"]
8+
schedule:
9+
- cron: "27 3 * * 1" # Mondays 03:27 UTC
10+
workflow_dispatch: {}
11+
12+
permissions: read-all
13+
14+
jobs:
15+
analysis:
16+
name: Scorecard analysis
17+
runs-on: ubuntu-latest
18+
permissions:
19+
security-events: write # upload SARIF to code scanning
20+
id-token: write # publish results to the OpenSSF API
21+
steps:
22+
- uses: actions/checkout@v6
23+
with:
24+
persist-credentials: false
25+
- uses: ossf/scorecard-action@v2.4.0
26+
with:
27+
results_file: results.sarif
28+
results_format: sarif
29+
publish_results: true
30+
- uses: github/codeql-action/upload-sarif@v3
31+
with:
32+
sarif_file: results.sarif

.github/workflows/stale.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Stale
2+
3+
# Marks inactive issues/PRs stale, then closes them after a grace period.
4+
on:
5+
schedule:
6+
- cron: "0 4 * * *" # daily 04:00 UTC
7+
workflow_dispatch: {}
8+
9+
permissions:
10+
issues: write
11+
pull-requests: write
12+
13+
jobs:
14+
stale:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/stale@v9
18+
with:
19+
days-before-stale: 60
20+
days-before-close: 14
21+
stale-issue-label: stale
22+
stale-pr-label: stale
23+
exempt-issue-labels: pinned,security,blocked
24+
exempt-pr-labels: pinned,security,blocked
25+
stale-issue-message: >
26+
This issue has been inactive for 60 days and is now marked stale.
27+
Comment to keep it open; it will close in 14 days otherwise.
28+
stale-pr-message: >
29+
This PR has been inactive for 60 days and is now marked stale.
30+
Push or comment to keep it open; it will close in 14 days otherwise.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Version Check
2+
3+
# Every PR into the default branch must bump package.json "version". The Release
4+
# workflow turns that version into a tag + GitHub Release on merge.
5+
on:
6+
pull_request:
7+
branches: ["master"]
8+
types: [opened, synchronize, reopened]
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
version-bumped:
15+
name: version bumped
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v6
19+
with:
20+
fetch-depth: 0 # need the base branch to diff the version
21+
persist-credentials: false
22+
- name: Read versions
23+
id: v
24+
run: |
25+
git fetch --no-tags --depth=1 origin "${{ github.base_ref }}"
26+
pr=$(jq -r .version package.json)
27+
base=$(git show "origin/${{ github.base_ref }}:package.json" | jq -r .version)
28+
echo "pr=$pr" >> "$GITHUB_OUTPUT"
29+
echo "base=$base" >> "$GITHUB_OUTPUT"
30+
- name: Compare
31+
env:
32+
PR: ${{ steps.v.outputs.pr }}
33+
BASE: ${{ steps.v.outputs.base }}
34+
run: |
35+
echo "base=$BASE pr=$PR"
36+
if [ "$PR" = "$BASE" ]; then
37+
echo "::error::package.json version not bumped (still $BASE). Bump it: patch for fixes, minor for features, major for breaking."
38+
exit 1
39+
fi
40+
greater=$(printf '%s\n%s\n' "$BASE" "$PR" | sort -V | tail -n1)
41+
if [ "$greater" != "$PR" ]; then
42+
echo "::error::package.json version $PR is lower than base $BASE; it must move forward."
43+
exit 1
44+
fi
45+
echo "Version bumped $BASE -> $PR ✓"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nextjs",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"private": true,
55
"scripts": {
66
"dev": "next dev -p 3000 --turbo",

0 commit comments

Comments
 (0)