diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0725db0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index 15ec2b7..bcc43e5 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,7 @@ !/flake.lock ### Repository configuration ### -!/.gitlab-ci.yml +!/.github/workflows/*.yml !/.husky/commit-msg !/conventional.config.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 5bf820e..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -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 diff --git a/conventional.config.js b/conventional.config.js index d5a12d1..c53caa5 100644 --- a/conventional.config.js +++ b/conventional.config.js @@ -15,14 +15,21 @@ const validateRepoScopedCommit = (message) => { } }; +const packagePrefix = "@websnacksjs/"; const packages = await fs.readdir(new URL("./packages", import.meta.url)); -const validScopes = ["repo", ...packages]; /** * @param {import("@websnacksjs/conventional").CommitMessage} message * @returns {void} */ 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"]; if (!supportedTypes.includes(message.type)) { 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({ validateCommitMessage(message) { if (!message.scope) { @@ -107,19 +47,25 @@ export default defineConfig({ ); } - if (message.scope === "repo") { - validateRepoScopedCommit(message); - } else if (packages.includes(message.scope)) { - validatePackageScopedCommit(message); - } else { + if (message.footers.length > 0) { throw new Error( - [ - `scope ${JSON.stringify(message.scope)} is unsupported`, - `(try one of ${JSON.stringify(validScopes).replace(",", ", ")})`, - ].join(" "), + `commit message footers are currently unsupported ` + + `(try removing them from your commit message)`, ); } - 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"])`, + ); }, });