diff --git a/.github/workflows/map-artifact.yml b/.github/workflows/map-artifact.yml new file mode 100644 index 0000000..7fc2e6e --- /dev/null +++ b/.github/workflows/map-artifact.yml @@ -0,0 +1,69 @@ +name: map-artifact + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-map-artifact: + name: Build compiled JSON-IR map artifact + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Checkout interscript-ts + run: git clone --depth=1 --branch main https://github.com/interscript/interscript-ts.git ../interscript-ts + - uses: actions/setup-node@v7 + with: + node-version: "22" + - name: Install + build TS converter + run: | + cd ../interscript-ts + npm ci + npm run build + - name: Build map artifact directory + run: npx -y tsx scripts/build-json-ir-artifact.mjs ../interscript-ts dist/interscript-maps-ir + - name: Validate artifact shape + run: | + test -f dist/interscript-maps-ir/manifest.json + test -f dist/interscript-maps-ir/SHA256SUMS + test "$(find dist/interscript-maps-ir/maps -name '*.json' | wc -l | tr -d ' ')" = "289" + node - <<'NODE' + import { createHash } from 'node:crypto' + import { readFileSync } from 'node:fs' + for (const line of readFileSync('dist/interscript-maps-ir/SHA256SUMS', 'utf8').trim().split('\n')) { + const [expected, path] = line.split(/ /) + const actual = createHash('sha256').update(readFileSync(`dist/interscript-maps-ir/${path}`)).digest('hex') + if (actual !== expected) throw new Error(`${path}: sha256 mismatch`) + } + NODE + - name: Pack artifact + run: | + version="$([ "${GITHUB_REF_TYPE}" = "tag" ] && echo "${GITHUB_REF_NAME#v}" || echo "${GITHUB_SHA::12}")" + mkdir -p dist/release + tar -C dist/interscript-maps-ir -czf "dist/release/interscript-maps-ir-${version}.tar.gz" . + node - <<'NODE' + import { createHash } from 'node:crypto' + import { readFileSync, writeFileSync } from 'node:fs' + const version = process.env.GITHUB_REF_TYPE === "tag" + ? process.env.GITHUB_REF_NAME.replace(/^v/, '') + : process.env.GITHUB_SHA.slice(0, 12) + const name = `interscript-maps-ir-${version}.tar.gz` + const sha = createHash('sha256').update(readFileSync(`dist/release/${name}`)).digest('hex') + writeFileSync(`dist/release/${name}.sha256`, `${sha} ${name}\n`) + NODE + - name: Upload workflow artifact + uses: actions/upload-artifact@v4 + with: + name: interscript-maps-ir + path: dist/release/ + - name: Attach artifact to GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "$GITHUB_REF_NAME" dist/release/* --clobber diff --git a/scripts/build-json-ir-artifact.mjs b/scripts/build-json-ir-artifact.mjs new file mode 100644 index 0000000..339e035 --- /dev/null +++ b/scripts/build-json-ir-artifact.mjs @@ -0,0 +1,76 @@ +#!/usr/bin/env node +// Build the deployable JSON-IR map artifact from this repository's .isc corpus. +// +// Usage: +// node scripts/build-json-ir-artifact.mjs +// +// Output directory layout: +// manifest.json +// SHA256SUMS +// maps/.json + +import { createHash } from "node:crypto" +import { mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs" +import { basename, join, relative, resolve } from "node:path" +import { pathToFileURL } from "node:url" + +const [tsDir, outDir] = process.argv.slice(2) +if (!tsDir || !outDir) { + console.error("usage: build-json-ir-artifact.mjs ") + process.exit(2) +} + +const repoRoot = resolve(import.meta.dirname, "..") +const mapsDir = join(repoRoot, "maps") +const outputRoot = resolve(outDir) +const outputMaps = join(outputRoot, "maps") + +const { parseIsc } = await import(pathToFileURL(resolve(tsDir, "src/isc/parser.ts"))) +const { iscToCompiledMap } = await import(pathToFileURL(resolve(tsDir, "src/isc/converter.ts"))) + +rmSync(outputRoot, { recursive: true, force: true }) +mkdirSync(outputMaps, { recursive: true }) + +const files = readdirSync(mapsDir) + .filter((file) => file.endsWith(".isc")) + .sort() + +const systems = [] +for (const file of files) { + const code = basename(file, ".isc") + const source = readFileSync(join(mapsDir, file), "utf8") + const doc = parseIsc(source, file) + const compiled = iscToCompiledMap(doc) + writeFileSync(join(outputMaps, `${code}.json`), `${JSON.stringify(compiled)}\n`) + systems.push(code) +} + +const manifest = { + schema: "interscript.maps.ir.v1", + source: "interscript/maps", + generatedAt: new Date().toISOString(), + count: systems.length, + systems, +} +writeFileSync(join(outputRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`) + +const artifactFiles = [] +function walk(dir) { + for (const name of readdirSync(dir).sort()) { + const path = join(dir, name) + if (statSync(path).isDirectory()) walk(path) + else artifactFiles.push(path) + } +} +walk(outputRoot) + +const checksums = artifactFiles + .filter((path) => basename(path) !== "SHA256SUMS") + .map((path) => { + const sha = createHash("sha256").update(readFileSync(path)).digest("hex") + return `${sha} ${relative(outputRoot, path)}` + }) + .join("\n") +writeFileSync(join(outputRoot, "SHA256SUMS"), `${checksums}\n`) + +console.log(`compiled ${systems.length} maps to ${outputRoot}`)