diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e49aef..1496b21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,91 +4,9 @@ on: push: pull_request: -env: - # Used to differentiate from other slices of the cache for other workflow runs, and to forcefully bust the cache when renamed - CACHE_KEY_BASE: ci@v1 - jobs: - setup: - name: Setup - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up Deno - uses: denoland/setup-deno@v2 - with: - deno-version: 2.x - cache-hash: ${{ env.CACHE_KEY_BASE }}-${{ hashFiles('deno.lock') }} - - - name: Install dependencies - run: deno install - - test: - name: Test - needs: [setup] - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - # Now the Deno cache should be warmed up, so this should be much faster - - name: Set up Deno - uses: denoland/setup-deno@v2 - with: - deno-version: 2.x - cache-hash: ${{ env.CACHE_KEY_BASE }}-${{ hashFiles('deno.lock') }} - - - name: Test - run: deno test -P --no-prompt --coverage --shuffle - - - name: Upload coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-report - path: | - coverage/html - coverage/lcov.info - - - name: Upload raw coverage files - uses: actions/upload-artifact@v4 - with: - name: coverage-report-raw - path: coverage/*.json - - lint: - name: Lint - needs: [setup] - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - # Now the Deno cache should be warmed up, so this should be much faster - - name: Set up Deno - uses: denoland/setup-deno@v2 - with: - deno-version: 2.x - cache-hash: ${{ env.CACHE_KEY_BASE }}-${{ hashFiles('deno.lock') }} - - - name: Lint - run: deno run --allow-env --allow-run --no-prompt npm:@biomejs/biome ci - - check: - name: Check - needs: [setup] - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - # Now the Deno cache should be warmed up, so this should be much faster - - name: Set up Deno - uses: denoland/setup-deno@v2 - with: - deno-version: 2.x - cache-hash: ${{ env.CACHE_KEY_BASE }}-${{ hashFiles('deno.lock') }} - - - name: Lint - run: deno check + run-procedure: + name: Run CI Procedure + uses: ./.github/workflows/fn-ci.yml + with: + cache-key-base: ci@v1 diff --git a/.github/workflows/fn-ci.yml b/.github/workflows/fn-ci.yml new file mode 100644 index 0000000..b9d2931 --- /dev/null +++ b/.github/workflows/fn-ci.yml @@ -0,0 +1,43 @@ +name: CI Procedure + +on: + workflow_call: + inputs: + cache-key-base: + description: A string used to differentiate from other slices of the cache for other workflow runs, and to forcefully bust the cache when renamed + type: string + required: true + +jobs: + main-checks: + name: Main Checks + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Deno + uses: denoland/setup-deno@v2 + with: + deno-version: 2.x + cache-hash: ${{ inputs.cache-key-base }}-${{ hashFiles('deno.lock') }} + + - name: Install dependencies + run: deno install + + - name: Run main script + run: deno run -P=ci @encode/ci + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: | + coverage/html + coverage/lcov.info + + - name: Upload raw coverage files + uses: actions/upload-artifact@v4 + with: + name: coverage-report-raw + path: coverage/*.json diff --git a/deno.jsonc b/deno.jsonc index d26610f..25641fe 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -8,17 +8,19 @@ }, "imports": { - // Import alias - "@root/": "./", - // External dependencies + "@actions/core": "npm:@actions/core@^1.11.1", + "@actions/github": "npm:@actions/github@^6.0.1", "@biomejs/biome": "npm:@biomejs/biome@2.3.6", "@std/assert": "jsr:@std/assert@1.0.16", + "@std/async": "jsr:@std/async@^1.0.15", "@std/testing": "jsr:@std/testing@1.0.16", "@std/fmt": "jsr:@std/fmt@1.0.8", "@std/cli": "jsr:@std/cli@1.0.24", "@std/fs": "jsr:@std/fs@1.0.20", "@std/path": "jsr:@std/path@1.1.3", - "chevrotain": "npm:chevrotain@^11.0.3" + "chevrotain": "npm:chevrotain@^11.0.3", + + "@root/": "./" }, "test": { @@ -31,6 +33,11 @@ "biome": { "env": true, "run": true + }, + + "ci": { + "env": true, + "run": ["deno"] } }, diff --git a/deno.lock b/deno.lock index 3bd68b1..0a9a588 100644 --- a/deno.lock +++ b/deno.lock @@ -3,6 +3,7 @@ "specifiers": { "jsr:@std/assert@1.0.16": "1.0.16", "jsr:@std/assert@^1.0.15": "1.0.16", + "jsr:@std/async@^1.0.15": "1.0.15", "jsr:@std/cli@1.0.24": "1.0.24", "jsr:@std/data-structures@^1.0.9": "1.0.9", "jsr:@std/fmt@1.0.8": "1.0.8", @@ -13,6 +14,9 @@ "jsr:@std/path@^1.1.2": "1.1.3", "jsr:@std/path@^1.1.3": "1.1.3", "jsr:@std/testing@1.0.16": "1.0.16", + "npm:@actions/core@^1.11.1": "1.11.1", + "npm:@actions/github@^6.0.1": "6.0.1_@octokit+core@5.2.2", + "npm:@biomejs/biome@*": "2.3.6", "npm:@biomejs/biome@2.3.6": "2.3.6", "npm:@types/node@*": "24.2.0", "npm:chevrotain@^11.0.3": "11.0.3" @@ -24,6 +28,9 @@ "jsr:@std/internal" ] }, + "@std/async@1.0.15": { + "integrity": "55d1d9d04f99403fe5730ab16bdcc3c47f658a6bf054cafb38a50f046238116e" + }, "@std/cli@1.0.24": { "integrity": "b655a5beb26aa94f98add6bc8889f5fb9bc3ee2cc3fc954e151201f4c4200a5e", "dependencies": [ @@ -59,6 +66,7 @@ "integrity": "a917ffdeb5924c9be436dc78bc32e511760e14d3a96e49c607fc5ecca86d0092", "dependencies": [ "jsr:@std/assert@^1.0.15", + "jsr:@std/async", "jsr:@std/data-structures", "jsr:@std/fs@^1.0.19", "jsr:@std/internal", @@ -67,6 +75,41 @@ } }, "npm": { + "@actions/core@1.11.1": { + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dependencies": [ + "@actions/exec", + "@actions/http-client" + ] + }, + "@actions/exec@1.1.1": { + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": [ + "@actions/io" + ] + }, + "@actions/github@6.0.1_@octokit+core@5.2.2": { + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", + "dependencies": [ + "@actions/http-client", + "@octokit/core", + "@octokit/plugin-paginate-rest", + "@octokit/plugin-rest-endpoint-methods", + "@octokit/request", + "@octokit/request-error", + "undici" + ] + }, + "@actions/http-client@2.2.3": { + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dependencies": [ + "tunnel", + "undici" + ] + }, + "@actions/io@1.1.3": { + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, "@biomejs/biome@2.3.6": { "integrity": "sha512-oqUhWyU6tae0MFsr/7iLe++QWRg+6jtUhlx9/0GmCWDYFFrK366sBLamNM7D9Y+c7YSynUFKr8lpEp1r6Sk7eA==", "optionalDependencies": [ @@ -145,12 +188,97 @@ "@chevrotain/utils@11.0.3": { "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==" }, + "@fastify/busboy@2.1.1": { + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==" + }, + "@octokit/auth-token@4.0.0": { + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==" + }, + "@octokit/core@5.2.2": { + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "dependencies": [ + "@octokit/auth-token", + "@octokit/graphql", + "@octokit/request", + "@octokit/request-error", + "@octokit/types@13.10.0", + "before-after-hook", + "universal-user-agent" + ] + }, + "@octokit/endpoint@9.0.6": { + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dependencies": [ + "@octokit/types@13.10.0", + "universal-user-agent" + ] + }, + "@octokit/graphql@7.1.1": { + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dependencies": [ + "@octokit/request", + "@octokit/types@13.10.0", + "universal-user-agent" + ] + }, + "@octokit/openapi-types@20.0.0": { + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "@octokit/openapi-types@24.2.0": { + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==" + }, + "@octokit/plugin-paginate-rest@9.2.2_@octokit+core@5.2.2": { + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "dependencies": [ + "@octokit/core", + "@octokit/types@12.6.0" + ] + }, + "@octokit/plugin-rest-endpoint-methods@10.4.1_@octokit+core@5.2.2": { + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dependencies": [ + "@octokit/core", + "@octokit/types@12.6.0" + ] + }, + "@octokit/request-error@5.1.1": { + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dependencies": [ + "@octokit/types@13.10.0", + "deprecation", + "once" + ] + }, + "@octokit/request@8.4.1": { + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dependencies": [ + "@octokit/endpoint", + "@octokit/request-error", + "@octokit/types@13.10.0", + "universal-user-agent" + ] + }, + "@octokit/types@12.6.0": { + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": [ + "@octokit/openapi-types@20.0.0" + ] + }, + "@octokit/types@13.10.0": { + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dependencies": [ + "@octokit/openapi-types@24.2.0" + ] + }, "@types/node@24.2.0": { "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", "dependencies": [ "undici-types" ] }, + "before-after-hook@2.2.3": { + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, "chevrotain@11.0.3": { "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "dependencies": [ @@ -162,21 +290,48 @@ "lodash-es" ] }, + "deprecation@2.3.1": { + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "lodash-es@4.17.21": { "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, + "once@1.4.0": { + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": [ + "wrappy" + ] + }, + "tunnel@0.0.6": { + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + }, "undici-types@7.10.0": { "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" + }, + "undici@5.29.0": { + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dependencies": [ + "@fastify/busboy" + ] + }, + "universal-user-agent@6.0.1": { + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "wrappy@1.0.2": { + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" } }, "workspace": { "dependencies": [ "jsr:@std/assert@1.0.16", + "jsr:@std/async@^1.0.15", "jsr:@std/cli@1.0.24", "jsr:@std/fmt@1.0.8", "jsr:@std/fs@1.0.20", "jsr:@std/path@1.1.3", "jsr:@std/testing@1.0.16", + "npm:@actions/core@^1.11.1", + "npm:@actions/github@^6.0.1", "npm:@biomejs/biome@2.3.6", "npm:chevrotain@^11.0.3" ] diff --git a/scripts/ci/deno.jsonc b/scripts/ci/deno.jsonc new file mode 100644 index 0000000..518bbf0 --- /dev/null +++ b/scripts/ci/deno.jsonc @@ -0,0 +1,14 @@ +{ + "name": "@encode/ci", + "version": "0.1.0", + + "exports": { + ".": "./main.ts", + + "./lib": "./mod.ts" + }, + + "imports": { + "@/": "./" + } +} diff --git a/scripts/ci/main.ts b/scripts/ci/main.ts new file mode 100644 index 0000000..564187f --- /dev/null +++ b/scripts/ci/main.ts @@ -0,0 +1,45 @@ +import { permissionArgs, runCommands } from '@encode/ci/lib'; + +export async function main(): Promise { + const commonCommandOptions: Deno.CommandOptions = { + stderr: 'piped', + stdout: 'piped', + }; + + await runCommands([ + { + name: 'Biome checks', + args: [ + Deno.execPath(), + { + ...commonCommandOptions, + args: ['run', ...permissionArgs('biome'), 'npm:@biomejs/biome', 'ci'], + }, + ], + }, + { + name: 'Deno checks', + args: [ + Deno.execPath(), + { + ...commonCommandOptions, + args: ['check'], + }, + ], + }, + { + name: 'Tests', + args: [ + Deno.execPath(), + { + ...commonCommandOptions, + args: ['test', ...permissionArgs(), '--coverage', '--shuffle'], + }, + ], + }, + ]); +} + +if (import.meta.main) { + await main(); +} diff --git a/scripts/ci/mod.ts b/scripts/ci/mod.ts new file mode 100644 index 0000000..74389ae --- /dev/null +++ b/scripts/ci/mod.ts @@ -0,0 +1,68 @@ +import { error } from '@actions/core'; +import { delay } from '@std/async'; +import { bold, red } from '@std/fmt/colors'; + +export interface CommandParams { + name: string; + args: ConstructorParameters; +} + +export async function runCommands( + commandParams: Iterable, +): Promise { + const processes = Array.from(commandParams, ({ name, args }) => { + return { name, process: new Deno.Command(...args).spawn() }; + }); + + for (const [idx, { name, process }] of processes.entries()) { + await Promise.all([ + process.stderr.pipeTo(Deno.stderr.writable, { preventClose: true, preventAbort: true }), + process.stdout.pipeTo(Deno.stdout.writable, { preventClose: true, preventAbort: true }), + ]); + + if (!(await process.status).success) { + console.log('\n'); + + annotateError({ title: 'CI', message: `${name} failed!` }); + } + + if (idx !== processes.length - 1) { + const width = (() => { + try { + return Deno.consoleSize().columns; + } catch { + return 10; + } + })(); + + console.log(`\n${bold('-'.repeat(width))}\n\n`); + } + + if (isCI()) { + await delay(500); + } + } +} + +export function isCI() { + return Deno.env.get('CI') === 'true'; +} + +export function permissionArgs(permissionSetName?: string): string[] { + return [permissionSetName ? `-P=${permissionSetName}` : '-P', '--no-prompt']; +} + +interface Annotation { + title: string; + message: string; +} + +function annotateError(annotation: Annotation): void { + const { title, message } = annotation; + + if (isCI()) { + error(red(message), { title }); + } else { + console.error(`${bold(`${title}:`)} ${red(message)}`); + } +}