Compare commits

..

4 commits

Author SHA1 Message Date
985dbc32cd
chore(repo): implement CI w/ github actions (#4)
Some checks failed
Continuous Integration (CI) / Node 22 (push) Has been cancelled
Continuous Integration (CI) / Node 24 (push) Has been cancelled
2025-08-20 09:15:43 +12:00
d6696a150b
test(@websnacksjs/i18n): fix broken tests missing translation messages (#5) 2025-08-20 08:57:15 +12:00
2867c2f875
chore(repo): dog-food @websnacksjs/conventional in this repo 2025-08-19 18:35:47 +12:00
5f519f54f2
feat(@websnacksjs/conventional): initial implementation
`@websnacksjs/conventional` is a cli program which can be used with
`husky` & git hooks to enforce that commit messages adhere to the
conventional commits standard.
2025-08-19 18:31:34 +12:00
4 changed files with 49 additions and 97 deletions

24
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Continuous Integration (CI)
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node: [22, 24]
name: Node ${{ matrix.node }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: "npm"
cache-dependency-path: "package-lock.json"
- run: npm ci
- run: npm run test --workspaces --if-present

2
.gitignore vendored
View file

@ -36,7 +36,7 @@
!/flake.lock !/flake.lock
### Repository configuration ### ### Repository configuration ###
!/.gitlab-ci.yml !/.github/workflows/*.yml
!/.husky/commit-msg !/.husky/commit-msg
!/conventional.config.js !/conventional.config.js

View file

@ -1,18 +0,0 @@
stages: [build, test]
build:
stage: build
cache: &cached-deps
key:
files: [package-lock.json]
paths: [node_modules/]
script:
- npm ci
- npm run build --workspaces --if-present
test:
stage: test
cache:
<<: &cached-deps
script:
- npm run test --workspaces --if-present

View file

@ -15,14 +15,21 @@ const validateRepoScopedCommit = (message) => {
} }
}; };
const packagePrefix = "@websnacksjs/";
const packages = await fs.readdir(new URL("./packages", import.meta.url)); const packages = await fs.readdir(new URL("./packages", import.meta.url));
const validScopes = ["repo", ...packages];
/** /**
* @param {import("@websnacksjs/conventional").CommitMessage} message * @param {import("@websnacksjs/conventional").CommitMessage} message
* @returns {void} * @returns {void}
*/ */
const validatePackageScopedCommit = (message) => { const validatePackageScopedCommit = (message) => {
const pkg = message.scope?.slice(packagePrefix.length) ?? "";
if (!packages.includes(pkg)) {
throw new Error(
`unknown package ${JSON.stringify(pkg)} referenced in commit scope`,
);
}
const supportedTypes = ["feat", "fix", "docs", "test", "chore"]; const supportedTypes = ["feat", "fix", "docs", "test", "chore"];
if (!supportedTypes.includes(message.type)) { if (!supportedTypes.includes(message.type)) {
throw new Error( throw new Error(
@ -32,73 +39,6 @@ const validatePackageScopedCommit = (message) => {
} }
}; };
/**
* @param {string} value
* @returns {void}
*/
const validateUrl = (value) => {
try {
new URL(value);
} catch {
const error = new Error(
`expected valid URL but got ${JSON.stringify(value)}`,
);
Error.captureStackTrace(error, validateUrl);
throw error;
}
};
/**
* @param {import("@websnacksjs/conventional").Footer[]} footers
* @returns void
*/
const validateFooters = (footers) => {
/** @type {string[]} */
const unsupportedFooters = [];
/** @type {{footer: string, reason: Error}[]} */
const invalidFooters = [];
for (const { key, value } of footers) {
try {
switch (key) {
case "Merge-request": {
validateUrl(value);
break;
}
default: {
unsupportedFooters.push(key);
}
}
} catch (error) {
if (!(error instanceof Error)) {
throw new Error(
`caught unexpected non-error value ${JSON.stringify(error)}`,
);
}
invalidFooters.push({ footer: key, reason: error });
}
}
const errorMessageParts = [];
if (unsupportedFooters.length > 0) {
let message = `unspported footers in message:`;
for (const footer of unsupportedFooters) {
message += `\n\t- ${footer}`;
}
errorMessageParts.push(message);
}
if (invalidFooters.length > 0) {
let message = `invalid footers in message:`;
for (const { footer, reason } of invalidFooters) {
message += `\n\t- ${footer}: ${reason.message}`;
}
errorMessageParts.push(message);
}
if (errorMessageParts.length > 0) {
const message = errorMessageParts.join("\n\n");
throw new Error(message);
}
};
export default defineConfig({ export default defineConfig({
validateCommitMessage(message) { validateCommitMessage(message) {
if (!message.scope) { if (!message.scope) {
@ -107,19 +47,25 @@ export default defineConfig({
); );
} }
if (message.scope === "repo") { if (message.footers.length > 0) {
validateRepoScopedCommit(message);
} else if (packages.includes(message.scope)) {
validatePackageScopedCommit(message);
} else {
throw new Error( throw new Error(
[ `commit message footers are currently unsupported ` +
`scope ${JSON.stringify(message.scope)} is unsupported`, `(try removing them from your commit message)`,
`(try one of ${JSON.stringify(validScopes).replace(",", ", ")})`,
].join(" "),
); );
} }
validateFooters(message.footers); if (message.scope === "repo") {
validateRepoScopedCommit(message);
return;
}
if (message.scope.startsWith(packagePrefix)) {
validatePackageScopedCommit(message);
return;
}
throw new Error(
`scope ${JSON.stringify(message.scope)} is unsupported (try one of ["repo", "@websnacksjs/:package"])`,
);
}, },
}); });