Skip to content

Commit 42de65b

Browse files
authored
release: v0.4.0 (#39)
* New branch (#33) * chore: add pending project files * refactor(ingest): centralize ingestion via parser/resolver/store layers * docs: document layered ingest architecture * test(perf): add qmd benchmark harness and non-blocking CI regression check * perf(bench): add ingest hotpath benchmark and record qmd optimization * perf(ingest): batch session writes and add stable benchmark tooling * Add benchmark scorecard to CI summary and sticky PR comment * Fix bench import path and temporarily disable design-contract workflow * CI: checkout qmd submodule in perf bench workflow * Fix Windows path handling in ingest session discovery * Feature/bench scorecard ci windows fixes (#34) * chore: add pending project files * refactor(ingest): centralize ingestion via parser/resolver/store layers * docs: document layered ingest architecture * test(perf): add qmd benchmark harness and non-blocking CI regression check * perf(bench): add ingest hotpath benchmark and record qmd optimization * perf(ingest): batch session writes and add stable benchmark tooling * Add benchmark scorecard to CI summary and sticky PR comment * Fix bench import path and temporarily disable design-contract workflow * CI: checkout qmd submodule in perf bench workflow * Fix Windows path handling in ingest session discovery * CI: run full test matrix only on merge branches * CI: auto-create draft prerelease on successful dev CI * CI: auto-template and title for dev to main PRs * CI: create dev draft release after successful dev test matrix * chore: add e2e dev release flow test marker (#36) * docs: add CI/release workflow architecture and north-star plan * ci: add commit lint, semver metadata, and deterministic release notes * docs: finalize workflow policy docs without backlog sections * ci: scope commit lint to pull request commit ranges only * fix(ci): setup bun before dev draft release metadata step * fix(ci): allow legacy non-conventional history for dev draft metadata * fix(release): align dev-main PR version with latest stable tag * ci: improve workflow and check naming for PR readability * ci: skip PR test job for dev to main release PRs
1 parent 71fbe06 commit 42de65b

9 files changed

Lines changed: 491 additions & 119 deletions

File tree

.github/workflows/ci.yml

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
name: CI
1+
name: CI Core
2+
3+
concurrency:
4+
group: ci-${{ github.workflow }}-${{ github.ref }}
5+
cancel-in-progress: true
26

37
on:
48
push:
@@ -8,8 +12,10 @@ on:
812

913
jobs:
1014
test-pr:
11-
if: github.event_name == 'pull_request'
12-
name: Test (ubuntu-latest)
15+
if: >
16+
github.event_name == 'pull_request' &&
17+
!(github.event.pull_request.head.ref == 'dev' && github.event.pull_request.base.ref == 'main')
18+
name: PR / Tests (ubuntu)
1319
runs-on: ubuntu-latest
1420

1521
steps:
@@ -32,7 +38,7 @@ jobs:
3238

3339
test-merge:
3440
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')
35-
name: Test (${{ matrix.os }})
41+
name: Push / Tests (${{ matrix.os }})
3642
runs-on: ${{ matrix.os }}
3743
strategy:
3844
fail-fast: false
@@ -58,7 +64,7 @@ jobs:
5864
run: bun test test/
5965

6066
dev-draft-release:
61-
name: Dev Draft Release
67+
name: Push(dev) / Draft Release
6268
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
6369
needs: test-merge
6470
runs-on: ubuntu-latest
@@ -72,11 +78,19 @@ jobs:
7278
fetch-depth: 0
7379
submodules: recursive
7480

81+
- name: Setup Bun
82+
uses: oven-sh/setup-bun@v2
83+
with:
84+
bun-version: latest
85+
86+
- name: Install dependencies
87+
run: bun install
88+
7589
- name: Compute dev tag
76-
id: tag
90+
id: meta
7791
run: |
78-
BASE_VERSION=$(node -p "require('./package.json').version")
79-
DEV_TAG="v${BASE_VERSION}-dev.${{ github.run_number }}"
92+
bun run scripts/release-meta.ts --allow-invalid --github-output "$GITHUB_OUTPUT"
93+
DEV_TAG="${{ steps.meta.outputs.next_version }}-dev.${{ github.run_number }}"
8094
echo "dev_tag=${DEV_TAG}" >> "$GITHUB_OUTPUT"
8195
8296
- name: Remove previous dev draft releases and tags
@@ -122,9 +136,10 @@ jobs:
122136
- name: Create draft prerelease
123137
uses: softprops/action-gh-release@v2
124138
with:
125-
tag_name: ${{ steps.tag.outputs.dev_tag }}
139+
tag_name: ${{ steps.meta.outputs.dev_tag }}
126140
target_commitish: ${{ github.sha }}
127-
name: Dev Draft ${{ steps.tag.outputs.dev_tag }}
128-
generate_release_notes: true
141+
name: Dev Draft ${{ steps.meta.outputs.dev_tag }}
142+
body: ${{ steps.meta.outputs.release_notes }}
143+
generate_release_notes: false
129144
draft: true
130145
prerelease: true

.github/workflows/commitlint.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: PR Commit Lint
2+
3+
on:
4+
pull_request:
5+
branches: [main, dev]
6+
7+
jobs:
8+
lint:
9+
name: PR / Commit Lint
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Setup Bun
21+
uses: oven-sh/setup-bun@v2
22+
with:
23+
bun-version: latest
24+
25+
- name: Lint commits (PR range)
26+
run: |
27+
bun run scripts/release-meta.ts \
28+
--mode lint \
29+
--from-ref "${{ github.event.pull_request.base.sha }}" \
30+
--to "${{ github.event.pull_request.head.sha }}"

.github/workflows/dev-main-pr-template.yml

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Dev->Main PR Autofill
1+
name: Dev->Main PR Metadata
22

33
on:
44
pull_request:
@@ -7,6 +7,7 @@ on:
77

88
jobs:
99
autofill:
10+
name: PR / Dev->Main Metadata
1011
if: github.event.pull_request.head.ref == 'dev' && github.event.pull_request.base.ref == 'main'
1112
runs-on: ubuntu-latest
1213
permissions:
@@ -29,8 +30,35 @@ jobs:
2930
const pull_number = context.payload.pull_request.number;
3031
3132
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
32-
const version = pkg.version || "0.0.0";
33-
const title = `release: v${version} (dev -> main)`;
33+
const pkgVersion = String(pkg.version || "0.0.0");
34+
35+
function parseSemver(v) {
36+
const m = String(v).match(/^v?(\d+)\.(\d+)\.(\d+)$/);
37+
if (!m) return [0, 0, 0];
38+
return [Number(m[1]), Number(m[2]), Number(m[3])];
39+
}
40+
41+
function cmp(a, b) {
42+
const pa = parseSemver(a);
43+
const pb = parseSemver(b);
44+
if (pa[0] !== pb[0]) return pa[0] - pb[0];
45+
if (pa[1] !== pb[1]) return pa[1] - pb[1];
46+
return pa[2] - pb[2];
47+
}
48+
49+
const tagRefs = await github.paginate(github.rest.repos.listTags, {
50+
owner,
51+
repo,
52+
per_page: 100,
53+
});
54+
const stableTags = tagRefs
55+
.map((t) => t.name)
56+
.filter((t) => /^v\d+\.\d+\.\d+$/.test(t));
57+
stableTags.sort((a, b) => cmp(a, b));
58+
const latestTag = stableTags.length ? stableTags[stableTags.length - 1].replace(/^v/, "") : "0.0.0";
59+
60+
const version = cmp(pkgVersion, latestTag) >= 0 ? pkgVersion : latestTag;
61+
const title = `release: v${version}`;
3462
3563
const commits = await github.paginate(github.rest.pulls.listCommits, {
3664
owner,
@@ -48,10 +76,21 @@ jobs:
4876
`<!-- AUTO-GENERATED:COMMITS -->\n${commitsText}\n<!-- /AUTO-GENERATED:COMMITS -->`
4977
);
5078
79+
const existingBody = context.payload.pull_request.body || "";
80+
const preserveManual = /<!-- AUTO-GENERATED:COMMITS -->[\s\S]*?<!-- \/AUTO-GENERATED:COMMITS -->/m.test(existingBody);
81+
const nextBody = preserveManual
82+
? existingBody
83+
.replace(/- Version: .*/m, `- Version: v${version}`)
84+
.replace(
85+
/<!-- AUTO-GENERATED:COMMITS -->[\s\S]*?<!-- \/AUTO-GENERATED:COMMITS -->/m,
86+
`<!-- AUTO-GENERATED:COMMITS -->\n${commitsText}\n<!-- /AUTO-GENERATED:COMMITS -->`
87+
)
88+
: body;
89+
5190
await github.rest.pulls.update({
5291
owner,
5392
repo,
5493
pull_number,
5594
title,
56-
body,
95+
body: nextBody,
5796
});

.github/workflows/perf-bench.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
name: Perf Bench (Non-blocking)
1+
name: Perf Bench
2+
3+
concurrency:
4+
group: perf-${{ github.workflow }}-${{ github.ref }}
5+
cancel-in-progress: true
26

37
on:
48
pull_request:
@@ -20,7 +24,7 @@ on:
2024

2125
jobs:
2226
bench:
23-
name: Run ci-small benchmark
27+
name: Bench / ci-small
2428
runs-on: ubuntu-latest
2529
permissions:
2630
contents: read

.github/workflows/release.yml

Lines changed: 19 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
name: Release
22

3+
concurrency:
4+
group: release-${{ github.ref }}
5+
cancel-in-progress: false
6+
37
on:
48
push:
59
tags:
@@ -30,112 +34,27 @@ jobs:
3034
- name: Run tests
3135
run: bun test test/
3236

33-
- name: Generate changelog from merged PRs
34-
id: changelog
35-
env:
36-
GH_TOKEN: ${{ github.token }}
37+
- name: Compute release metadata from conventional commits
38+
id: meta
3739
run: |
38-
VERSION="${GITHUB_REF_NAME#v}"
39-
TAG="${GITHUB_REF_NAME}"
40-
41-
# Find previous tag
42-
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
43-
44-
if [ -z "$PREV_TAG" ]; then
45-
echo "No previous tag found, using all merged PRs"
46-
PREV_DATE="2000-01-01"
47-
else
48-
PREV_DATE=$(git log -1 --format=%aI "$PREV_TAG" | cut -dT -f1)
49-
fi
50-
51-
NOW=$(date -u +%Y-%m-%d)
52-
53-
# Fetch merged PRs between previous tag date and now
54-
PRS=$(gh pr list \
55-
--state merged \
56-
--search "merged:${PREV_DATE}..${NOW}" \
57-
--json number,title,mergedAt \
58-
--limit 100 \
59-
--jq '.[] | "\(.number)\t\(.title)"' 2>/dev/null || echo "")
60-
61-
# Categorize PRs by conventional commit prefix
62-
FIXED=""
63-
ADDED=""
64-
CHANGED=""
65-
DOCS=""
66-
OTHER=""
67-
68-
while IFS=$'\t' read -r num title; do
69-
[ -z "$num" ] && continue
70-
entry="- ${title} ([#${num}](https://github.com/${{ github.repository }}/pull/${num}))"
71-
72-
case "$title" in
73-
fix:*|fix\(*) FIXED="${FIXED}${entry}"$'\n' ;;
74-
feat:*|feat\(*) ADDED="${ADDED}${entry}"$'\n' ;;
75-
chore:*|chore\(*|refactor:*|refactor\(*|perf:*|perf\(*) CHANGED="${CHANGED}${entry}"$'\n' ;;
76-
docs:*|docs\(*) DOCS="${DOCS}${entry}"$'\n' ;;
77-
*) OTHER="${OTHER}${entry}"$'\n' ;;
78-
esac
79-
done <<< "$PRS"
80-
81-
# Build release notes
82-
NOTES=""
83-
84-
if [ -n "$FIXED" ]; then
85-
NOTES="${NOTES}### Fixed"$'\n\n'"${FIXED}"$'\n'
86-
fi
87-
if [ -n "$ADDED" ]; then
88-
NOTES="${NOTES}### Added"$'\n\n'"${ADDED}"$'\n'
89-
fi
90-
if [ -n "$CHANGED" ]; then
91-
NOTES="${NOTES}### Changed"$'\n\n'"${CHANGED}"$'\n'
92-
fi
93-
if [ -n "$DOCS" ]; then
94-
NOTES="${NOTES}### Documentation"$'\n\n'"${DOCS}"$'\n'
95-
fi
96-
if [ -n "$OTHER" ]; then
97-
NOTES="${NOTES}### Other"$'\n\n'"${OTHER}"$'\n'
98-
fi
99-
100-
# Fallback: if no PRs found, use auto-generated notes
101-
if [ -z "$(echo "$NOTES" | tr -d '[:space:]')" ]; then
102-
echo "HAS_NOTES=false" >> "$GITHUB_OUTPUT"
103-
else
104-
echo "HAS_NOTES=true" >> "$GITHUB_OUTPUT"
40+
bun run scripts/release-meta.ts \
41+
--current-tag "${GITHUB_REF_NAME}" \
42+
--to "${GITHUB_SHA}" \
43+
--github-output "$GITHUB_OUTPUT"
10544
106-
# Build full changelog entry
107-
CHANGELOG_ENTRY="## [${VERSION}] - ${NOW}"$'\n\n'"${NOTES}"
108-
109-
# Prepend to CHANGELOG.md
110-
if [ -f CHANGELOG.md ]; then
111-
# Insert after the header line(s)
112-
echo "${CHANGELOG_ENTRY}" | cat - CHANGELOG.md > CHANGELOG.tmp
113-
mv CHANGELOG.tmp CHANGELOG.md
114-
else
115-
printf '%s\n\n%s' "# Changelog" "${CHANGELOG_ENTRY}" > CHANGELOG.md
116-
fi
117-
118-
# Save notes for release body
119-
{
120-
echo "RELEASE_NOTES<<EOF"
121-
echo "$NOTES"
122-
echo "EOF"
123-
} >> "$GITHUB_OUTPUT"
124-
fi
125-
126-
- name: Commit CHANGELOG.md
127-
if: steps.changelog.outputs.HAS_NOTES == 'true'
45+
- name: Validate tag matches semantic bump
12846
run: |
129-
git config user.name "github-actions[bot]"
130-
git config user.email "github-actions[bot]@users.noreply.github.com"
131-
git add CHANGELOG.md
132-
git commit -m "docs: update CHANGELOG.md for ${GITHUB_REF_NAME} [skip ci]"
133-
git push origin HEAD:main
47+
EXPECTED="${{ steps.meta.outputs.next_version }}"
48+
ACTUAL="${GITHUB_REF_NAME}"
49+
if [ "$EXPECTED" != "$ACTUAL" ]; then
50+
echo "Tag/version mismatch: expected ${EXPECTED}, got ${ACTUAL}"
51+
exit 1
52+
fi
13453
13554
- name: Create GitHub Release
13655
uses: softprops/action-gh-release@v2
13756
with:
138-
body: ${{ steps.changelog.outputs.HAS_NOTES == 'true' && steps.changelog.outputs.RELEASE_NOTES || '' }}
139-
generate_release_notes: ${{ steps.changelog.outputs.HAS_NOTES != 'true' }}
57+
body: ${{ steps.meta.outputs.release_notes }}
58+
generate_release_notes: false
14059
draft: false
14160
prerelease: ${{ contains(github.ref_name, '-') }}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# CI Hardening Execution State
2+
3+
Last updated: 2026-02-27
4+
5+
## Scope
6+
Implements the requested improvements except design-contract re-enable (explicitly deferred).
7+
8+
## Completed in This Change
9+
- Added conventional-commit driven release metadata engine:
10+
- `scripts/release-meta.ts`
11+
- Added commit lint workflow:
12+
- `.github/workflows/commitlint.yml`
13+
- Updated `CI` dev draft release to derive semver + notes from commits:
14+
- `.github/workflows/ci.yml`
15+
- Updated stable release workflow to:
16+
- validate pushed tag against computed semver
17+
- generate release notes from exact conventional commits
18+
- stop mutating `main` during release
19+
- `.github/workflows/release.yml`
20+
- Updated dev->main PR title format:
21+
- removed `dev -> main` suffix from title
22+
- preserve manual PR body sections while refreshing autogenerated block
23+
- `.github/workflows/dev-main-pr-template.yml`
24+
- Added concurrency controls:
25+
- `ci.yml`, `perf-bench.yml`, `release.yml`
26+
27+
## North Star
28+
No bad release should be publishable without:
29+
1. passing required checks,
30+
2. semver consistency,
31+
3. conventional commit compliance,
32+
4. deterministic release notes from the actual commit set.
33+
34+
## Operating Rules
35+
- Merge strategy for protected branches should preserve conventional commit subjects
36+
(squash merge title must be conventional).
37+
- Do not bypass commit lint for release-bearing branches.
38+
- Any temporary workflow disable must include expiry date and tracking issue.

0 commit comments

Comments
 (0)