diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b1e47c..e3cf5c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,6 +76,5 @@ jobs: build/manifest.json build/main.js build/styles.css - build/versions.json env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 321383a..3afd148 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The release is self-contained. You do not need `pngquant`, `mozjpeg`, Homebrew, Source lives in `src-ts`. Root `main.js` is generated and ignored, so readable TypeScript rather than compiled output stays in Git. `npm run build` creates a production-minified local bundle; `npm run test:release` rebuilds it twice, requires deterministic bytes, validates inline WASM, and stages the release allowlist. -The GitHub release artifact contains only Obsidian install files: `manifest.json`, `main.js`, `styles.css`, and `versions.json`. Production minification is not obfuscation: the complete source remains readable in the repository. Tags are exact numeric SemVer without a `v` prefix. See [RELEASE_POLICY.md](RELEASE_POLICY.md). +The GitHub release artifact contains only Obsidian install files: `manifest.json`, `main.js`, and `styles.css`. `versions.json` stays in the repository for compatibility metadata, but it is not uploaded as a release asset. Production minification is not obfuscation: the complete source remains readable in the repository. Tags are exact numeric SemVer without a `v` prefix. See [RELEASE_POLICY.md](RELEASE_POLICY.md). ### Commands - **Compress all images in note**: Processes images referenced/used in the active note. diff --git a/README.ru.md b/README.ru.md index f716b6f..ac5c124 100644 --- a/README.ru.md +++ b/README.ru.md @@ -15,7 +15,7 @@ Release самодостаточный. Устанавливать `pngquant`, ` Исходники находятся в `src-ts`. Корневой `main.js` генерируется и игнорируется Git, поэтому в репозитории остаётся читаемый TypeScript, а не compiled output. `npm run build` создаёт production-minified локальный bundle; `npm run test:release` собирает его дважды, проверяет детерминированные байты, inline WASM и точный allowlist релиза. -GitHub release artifact содержит только install-файлы Obsidian: `manifest.json`, `main.js`, `styles.css` и `versions.json`. Минификация не является обфускацией: полный читаемый исходный код остаётся в репозитории. Теги имеют точный numeric SemVer без префикса `v`. См. [RELEASE_POLICY.md](RELEASE_POLICY.md). +GitHub release artifact содержит только install-файлы Obsidian: `manifest.json`, `main.js` и `styles.css`. `versions.json` остаётся в репозитории как compatibility metadata, но не загружается как release asset. Минификация не является обфускацией: полный читаемый исходный код остаётся в репозитории. Теги имеют точный numeric SemVer без префикса `v`. См. [RELEASE_POLICY.md](RELEASE_POLICY.md). ### Возможности - **Локальное сжатие**: PNG через libimagequant-wasm и PNG WASM decode; JPEG через mozjpeg-wasm. diff --git a/RELEASE_POLICY.md b/RELEASE_POLICY.md index ef5e306..1c60d06 100644 --- a/RELEASE_POLICY.md +++ b/RELEASE_POLICY.md @@ -23,7 +23,7 @@ installed production bundle locally or in CI. 3. Rebuilds production output and requires byte-identical SHA-256 results. 4. Runs `verify:root-ts` to prove root `main.js` equals the generated production artifact. -5. Recreates `build/` from an explicit four-file allowlist. +5. Recreates `build/` from the explicit Obsidian install-file allowlist. The production build has no source map and keeps WASM codecs inline, so users do not need package dependencies, external binaries, or separate `.wasm` @@ -40,7 +40,9 @@ GitHub release assets are exactly: - `manifest.json` - `main.js` - `styles.css` -- `versions.json` + +`versions.json` remains tracked in the repository for compatibility metadata, +but it is not a GitHub Release asset. The ignored `build/` staging directory is recreated on every release. It must not contain source workspaces, package metadata, caches, settings, backups, diff --git a/package.json b/package.json index b4ee6a4..0c0f449 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "files": [ "manifest.json", "main.js", - "styles.css", - "versions.json" + "styles.css" ], "scripts": { "lint": "node scripts/class-wide-gates.js", diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index 798a5fa..4a01ce5 100644 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -6,7 +6,7 @@ const { resolveRepositoryLayout } = require("./repository-layout"); const { repositoryRoot: root } = resolveRepositoryLayout(); const buildDir = path.join(root, "build"); -const releaseFiles = ["manifest.json", "main.js", "styles.css", "versions.json"]; +const releaseFiles = ["manifest.json", "main.js", "styles.css"]; fs.rmSync(buildDir, { recursive: true, force: true }); fs.mkdirSync(buildDir, { recursive: true }); diff --git a/scripts/smoke-ts.js b/scripts/smoke-ts.js index 7afe18d..0ab7a7a 100644 --- a/scripts/smoke-ts.js +++ b/scripts/smoke-ts.js @@ -973,12 +973,18 @@ if (isDevLayout) { assert(rootPackageSource.scripts.build === "npm run build:root", "Standalone package.json build must use the local source build"); assert(rootPackageSource.scripts.test.includes("npm run test:ts") && rootPackageSource.scripts["test:release"].includes("npm run verify:release"), "Standalone package.json test scripts must run local source and release verification"); } -assert(Array.isArray(rootPackageSource.files) && JSON.stringify(rootPackageSource.files) === JSON.stringify(["manifest.json", "main.js", "styles.css", "versions.json"]), "Root package.json files allowlist must contain only Obsidian install artifacts"); +assert(Array.isArray(rootPackageSource.files) && JSON.stringify(rootPackageSource.files) === JSON.stringify(["manifest.json", "main.js", "styles.css"]), "Root package.json files allowlist must contain only Obsidian install artifacts"); assert(!releaseWorkflowSource.includes("build/package.json") && !releaseWorkflowSource.includes("build/README.md") && releaseWorkflowSource.includes("npm run test:release"), "Release workflow still ships dev package metadata or bypasses root test:release"); assert(releaseWorkflowSource.includes('"*.*.*"') && releaseWorkflowSource.includes("^[0-9]+\\.[0-9]+\\.[0-9]+$") && !releaseWorkflowSource.includes('"v*"') && !releaseWorkflowSource.includes("GITHUB_REF_NAME#v"), "Release workflow does not combine a dotted tag trigger with exact numeric SemVer validation"); assert((releaseWorkflowSource.match(/actions\/checkout@v6/g) || []).length === 2 && (releaseWorkflowSource.match(/actions\/setup-node@v6/g) || []).length === 2 && (releaseWorkflowSource.match(/node-version:\s*"24"/g) || []).length === 2, "Release workflow must use checkout/setup-node v6 and Node 24 in both jobs"); const releasePrepareCommand = isDevLayout ? "npm --prefix source-recovery run prepare:release" : "npm run prepare:release"; -assert(releaseWorkflowSource.includes(releasePrepareCommand) && prepareReleaseSource.includes('["manifest.json", "main.js", "styles.css", "versions.json"]'), "Release workflow does not use the exact install-file staging allowlist"); +assert( + releaseWorkflowSource.includes(releasePrepareCommand) + && prepareReleaseSource.includes('["manifest.json", "main.js", "styles.css"]') + && !releaseWorkflowSource.includes("build/versions.json") + && !prepareReleaseSource.includes('"versions.json"'), + "Release workflow does not use the exact supported Obsidian install-file staging allowlist" +); assert(validateManifestSource.includes("forbiddenReleaseEntries") && validateManifestSource.includes("package.json must declare a files allowlist"), "Manifest validation does not guard release packaging against dev artifact leaks"); assert(validateManifestSource.includes("MIN_API_SURFACE_APP_VERSION") && validateManifestSource.includes("activeWindow/activeDocument/getBasePath"), "Manifest validation does not enforce API-surface minAppVersion"); assert(validateManifestSource.includes("manifest.json authorUrl must be a valid URL") && validateManifestSource.includes("must not point to localhost"), "Manifest validation does not reject malformed or local authorUrl values"); diff --git a/scripts/validate-manifest.js b/scripts/validate-manifest.js index deeb716..d4eeca2 100644 --- a/scripts/validate-manifest.js +++ b/scripts/validate-manifest.js @@ -136,8 +136,7 @@ for (const [version, minAppVersion] of Object.entries(versions)) { const releaseFiles = [ "manifest.json", "main.js", - "styles.css", - "versions.json" + "styles.css" ]; const forbiddenReleaseEntries = [ "cache-backups", @@ -174,7 +173,9 @@ assert( assert(releaseWorkflow.includes('"*.*.*"'), "Release workflow must trigger on dotted tag candidates"); assert(releaseWorkflow.includes("^[0-9]+\\.[0-9]+\\.[0-9]+$"), "Release workflow must validate exact numeric SemVer tags"); assert(!releaseWorkflow.includes("GITHUB_REF_NAME#v") && !releaseWorkflow.includes('"v*"'), "Release workflow must reject v-prefixed tags"); -assert(releasePrepare.includes('["manifest.json", "main.js", "styles.css", "versions.json"]'), "Release preparation script must use the explicit install-file allowlist"); +assert(releasePrepare.includes('["manifest.json", "main.js", "styles.css"]'), "Release preparation script must use the explicit install-file allowlist"); +assert(!releaseWorkflow.includes("build/versions.json"), "Release workflow must not upload versions.json as a GitHub Release asset"); +assert(!releasePrepare.includes('"versions.json"'), "Release preparation script must not stage versions.json as a GitHub Release asset"); for (const pattern of [ /^node_modules\/$/m, /^main\.js$/m, diff --git a/scripts/verify-release.js b/scripts/verify-release.js index 51271e3..72ee7ff 100644 --- a/scripts/verify-release.js +++ b/scripts/verify-release.js @@ -8,7 +8,7 @@ const { resolveRepositoryLayout } = require("./repository-layout"); const { repositoryRoot: root, sourceRoot } = resolveRepositoryLayout(); const rootMainPath = path.join(root, "main.js"); -const releaseFiles = ["manifest.json", "main.js", "styles.css", "versions.json"]; +const releaseFiles = ["manifest.json", "main.js", "styles.css"]; function sha256(buffer) { return crypto.createHash("sha256").update(buffer).digest("hex").toUpperCase();