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