Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 286 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,32 @@ name: CI

on:
pull_request:
push:
types:
- opened
- synchronize
- reopened
- closed
branches:
- master
- main
push:
branches:
- master
- main

permissions:
contents: read

jobs:
verify:
test:
name: Test and coverage
runs-on: ubuntu-latest

steps:
- name: Checkout
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
Expand All @@ -24,19 +36,284 @@ jobs:
- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Lint
- name: Run lint
run: yarn lint

- name: Typecheck
- name: Run typecheck
run: yarn typecheck

- name: Test
run: yarn test
- name: Run tests with coverage
run: yarn build && yarn c8 --all --src src --include "src/**/*.ts" --extension .ts --exclude-after-remap --reporter text --reporter lcov --check-coverage=false node --test "tests/**/*.test.mjs"

- name: Build docs
run: yarn docs:build

- name: Upload coverage
- name: Check npm package contents
run: npm pack --dry-run

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage/lcov.info
slug: haskou/ddd-kernel
verbose: true
fail_ci_if_error: false

publish:
name: Publish to npm
runs-on: ubuntu-latest
needs: test
concurrency:
group: publish-${{ github.repository }}-${{ github.event.pull_request.base.ref }}
cancel-in-progress: false
permissions:
contents: write
id-token: write
pull-requests: read
if: >-
github.event_name == 'pull_request' &&
github.event.action == 'closed' &&
github.event.pull_request.merged == true &&
(
github.event.pull_request.base.ref == 'master' ||
github.event.pull_request.base.ref == 'main'
) &&
(
github.event.pull_request.head.ref == 'feat' ||
startsWith(github.event.pull_request.head.ref, 'feat/') ||
github.event.pull_request.head.ref == 'fix' ||
startsWith(github.event.pull_request.head.ref, 'fix/') ||
github.event.pull_request.head.ref == 'break' ||
startsWith(github.event.pull_request.head.ref, 'break/')
)

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.base.ref }}
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: yarn
registry-url: https://registry.npmjs.org/

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Build package
run: yarn build

- name: Compute release version
id: release
shell: bash
run: |
set -euo pipefail

package_name="$(node -p "require('./package.json').name")"
current_version="$(node -p "require('./package.json').version")"
published_version="$(npm view "$package_name" version 2>/dev/null || true)"
branch_name="${HEAD_BRANCH}"
release_marker="for #${PR_NUMBER}"

echo "already_released=false" >> "$GITHUB_OUTPUT"

case "$branch_name" in
fix|fix/*)
bump="patch"
;;
feat|feat/*)
bump="minor"
;;
break|break/*)
bump="major"
;;
*)
echo "Unsupported release branch: $branch_name"
exit 1
;;
esac

release_subject="$(git log --format=%s --grep="$release_marker" -n 1 HEAD || true)"

if [ -n "$release_subject" ] && grep -q "$release_marker" <<< "$release_subject"; then
next_version="$(
node - "$release_subject" "$current_version" <<'NODE'
const [subject, fallbackVersion] = process.argv.slice(2);
const match = subject.match(/Release v([^ ]+) for #\d+/);

process.stdout.write(match?.[1] ?? fallbackVersion);
NODE
)"

echo "Release for PR #${PR_NUMBER} already exists."
echo "already_released=true" >> "$GITHUB_OUTPUT"
echo "package_name=$package_name" >> "$GITHUB_OUTPUT"
echo "current_version=$current_version" >> "$GITHUB_OUTPUT"
echo "published_version=$published_version" >> "$GITHUB_OUTPUT"
echo "next_version=$next_version" >> "$GITHUB_OUTPUT"
echo "bump=$bump" >> "$GITHUB_OUTPUT"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi

if [ -z "$published_version" ]; then
next_version="$current_version"
should_publish="true"
bump="initial"
else
next_version="$(
node - "$current_version" "$bump" <<'NODE'
const [version, bump] = process.argv.slice(2);
const parts = version.split('.').map(Number);

if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part))) {
throw new Error(`Unsupported npm version: ${version}`);
}

if (bump === 'patch') parts[2] += 1;
if (bump === 'minor') {
parts[1] += 1;
parts[2] = 0;
}
if (bump === 'major') {
parts[0] += 1;
parts[1] = 0;
parts[2] = 0;
}

process.stdout.write(parts.join('.'));
NODE
)"

should_publish="true"

if [ "$published_version" = "$next_version" ]; then
should_publish="false"
fi
fi

if [ "$current_version" != "$next_version" ]; then
npm version "$next_version" --no-git-tag-version
fi

if [ -n "$published_version" ] && [ "$should_publish" = "true" ]; then
node - "$published_version" "$next_version" <<'NODE'
const [publishedVersion, nextVersion] = process.argv.slice(2);
const parse = (version) => version.split('.').map(Number);
const published = parse(publishedVersion);
const next = parse(nextVersion);
const comparison = published.findIndex((part, index) => part !== next[index]);

if (comparison !== -1 && published[comparison] > next[comparison]) {
throw new Error(
`Published npm version ${publishedVersion} is newer than computed release ${nextVersion}`,
);
}
NODE
fi

echo "package_name=$package_name" >> "$GITHUB_OUTPUT"
echo "current_version=$current_version" >> "$GITHUB_OUTPUT"
echo "published_version=$published_version" >> "$GITHUB_OUTPUT"
echo "next_version=$next_version" >> "$GITHUB_OUTPUT"
echo "bump=$bump" >> "$GITHUB_OUTPUT"
echo "should_publish=$should_publish" >> "$GITHUB_OUTPUT"
env:
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}

- name: Publish package
if: >-
steps.release.outputs.already_released != 'true' &&
steps.release.outputs.should_publish == 'true'
run: npm publish --access public --tag latest

- name: Commit release version
if: steps.release.outputs.already_released != 'true'
shell: bash
run: |
set -euo pipefail

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git add package.json

if [ -f yarn.lock ]; then
git add yarn.lock
fi

if git diff --cached --quiet; then
echo "No package version changes to commit."
else
git commit -m "chore(release): 🔖 Release v${NEXT_VERSION} for #${PR_NUMBER}"
fi

if git rev-parse "v${NEXT_VERSION}" >/dev/null 2>&1; then
echo "Tag v${NEXT_VERSION} already exists."
else
git tag -a "v${NEXT_VERSION}" -m "Release v${NEXT_VERSION} for #${PR_NUMBER}"
fi

auth_header="$(
node -e "process.stdout.write('AUTHORIZATION: basic ' + Buffer.from('x-access-token:' + process.env.GITHUB_TOKEN).toString('base64'))"
)"
git -c "http.https://github.com/.extraheader=$auth_header" push origin "HEAD:${BASE_BRANCH}"
git -c "http.https://github.com/.extraheader=$auth_header" push origin "v${NEXT_VERSION}"
env:
BASE_BRANCH: ${{ github.event.pull_request.base.ref }}
NEXT_VERSION: ${{ steps.release.outputs.next_version }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ github.token }}

- name: Create GitHub release notes
if: steps.release.outputs.next_version != ''
shell: bash
run: |
set -euo pipefail

tag="v${NEXT_VERSION}"
release_notes="$(mktemp)"
pr_title="$(gh pr view "$PR_NUMBER" --json title --jq '.title')"
pr_body="$(gh pr view "$PR_NUMBER" --json body --jq '.body // ""')"
pr_url="$(gh pr view "$PR_NUMBER" --json url --jq '.url')"

{
echo "## Changes"
echo
printf -- "- %s\n" "$pr_title"
echo
echo "## Release details"
echo
printf -- "- npm: https://www.npmjs.com/package/%s/v/%s\n" "$PACKAGE_NAME" "$NEXT_VERSION"
printf -- "- Pull request: %s\n" "$pr_url"
printf -- "- Source branch: \`%s\`\n" "$HEAD_BRANCH"
printf -- "- Version bump: \`%s\`\n" "$BUMP"
} > "$release_notes"

if [ -n "$pr_body" ]; then
{
echo
echo "## Pull request notes"
echo
printf '%s\n' "$pr_body" | sed '/^------$/,$d'
} >> "$release_notes"
fi

if gh release view "$tag" >/dev/null 2>&1; then
gh release edit "$tag" --title "$tag" --notes-file "$release_notes"
else
gh release create "$tag" --verify-tag --title "$tag" --notes-file "$release_notes"
fi
env:
BUMP: ${{ steps.release.outputs.bump }}
GH_TOKEN: ${{ github.token }}
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
NEXT_VERSION: ${{ steps.release.outputs.next_version }}
PACKAGE_NAME: ${{ steps.release.outputs.package_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
Loading