fix(releases): проверять манифест и достоверность сведений о сборке
This commit is contained in:
1 parent
d5edfe5ef6
commit
1d27127b52
10 files changed
+135
-64
No files matched your search
+2
-1
@@ -1,3 +1,4 @@
|
||||
import {safeReleaseUrl} from "./release-metadata"
|
||||
import packageJson from "@/package.json"
|
||||
|
||||
const packageVersion = packageJson.version
|
||||
@@ -12,5 +13,5 @@ export function formatAppVersionLabel(version = getAppVersion()): string {
|
||||
|
||||
export function getReleaseUrl(): string | null {
|
||||
const value = process.env.NEXT_PUBLIC_RELEASE_URL?.trim()
|
||||
return value || null
|
||||
return safeReleaseUrl(value)
|
||||
}
|
||||
+6
-35
@@ -1,36 +1,7 @@
|
||||
import { readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
export interface ReleaseCommit {
|
||||
sha: string
|
||||
shortSha: string
|
||||
subject: string
|
||||
author: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface ReleaseManifest {
|
||||
version: string
|
||||
tag: string
|
||||
publishedAt: string | null
|
||||
releaseUrl: string
|
||||
commits: ReleaseCommit[]
|
||||
}
|
||||
|
||||
const fallbackManifest: ReleaseManifest = {
|
||||
version: "1.0.0",
|
||||
tag: "v1.0.0",
|
||||
publishedAt: null,
|
||||
releaseUrl: "",
|
||||
commits: [],
|
||||
}
|
||||
|
||||
export function readReleaseManifest(): ReleaseManifest {
|
||||
try {
|
||||
const filePath = join(process.cwd(), "public", "release-manifest.json")
|
||||
const raw = readFileSync(filePath, "utf8")
|
||||
return JSON.parse(raw) as ReleaseManifest
|
||||
} catch {
|
||||
return fallbackManifest
|
||||
}
|
||||
import {readFileSync} from "node:fs"
|
||||
import {join} from "node:path"
|
||||
import {parseReleaseManifest,type ReleaseManifest} from "./release-metadata"
|
||||
export type {ReleaseManifest,ReleaseCommit} from "./release-metadata"
|
||||
export function readReleaseManifest():ReleaseManifest|null {
|
||||
try{return parseReleaseManifest(JSON.parse(readFileSync(join(process.cwd(),"public","release-manifest.json"),"utf8")))}catch{return null}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import assert from "node:assert/strict"
|
||||
import {test} from "node:test"
|
||||
import {parseReleaseManifest,safeReleaseUrl,shouldShowReleaseNotes} from "./release-metadata"
|
||||
const empty={version:"1.0.0",tag:"v1.0.0",publishedAt:null,releaseUrl:"",commits:[]}
|
||||
const commit={sha:"a".repeat(40),shortSha:"a".repeat(7),subject:"fix(ui): исправить форму",author:"Fixture",type:"fix"}
|
||||
test("release metadata distinguishes confirmed empty and rejects malformed/inconsistent manifests",()=>{
|
||||
assert.deepEqual(parseReleaseManifest(empty),empty)
|
||||
for(const row of [null,[],{}, {...empty,commits:null},{...empty,publishedAt:"invalid"},{...empty,publishedAt:"2026-01-01"},{...empty,tag:"v2.0.0"},{...empty,commits:[{...commit,subject:12}]},{...empty,commits:[commit,commit]},{...empty,commits:[{...commit,shortSha:"bbbbbbb"}]}])assert.equal(parseReleaseManifest(row),null)
|
||||
assert.equal(parseReleaseManifest({...empty,publishedAt:"2026-10-03T00:00:00Z",commits:[commit]})?.commits.length,1)
|
||||
})
|
||||
test("release links allow only absolute HTTP(S) without credentials",()=>{
|
||||
for(const link of ["javascript:alert(1)","data:text/html,fixture","//example.test/releases","/releases","https://user:secret@example.test/releases",null]){assert.equal(safeReleaseUrl(link),null);assert.equal(parseReleaseManifest({...empty,releaseUrl:link}),null)}
|
||||
assert.equal(safeReleaseUrl(" https://example.test/releases "),"https://example.test/releases")
|
||||
assert.equal(parseReleaseManifest({...empty,releaseUrl:"http://gitea.example.test/releases"})?.releaseUrl,"http://gitea.example.test/releases")
|
||||
})
|
||||
|
||||
test("release announcement belongs only to the current production build",()=>{
|
||||
const manifest={...empty,commits:[commit]};assert.equal(shouldShowReleaseNotes(manifest,"1.0.0"),true);assert.equal(shouldShowReleaseNotes(manifest,"1.0.0-dev"),false);assert.equal(shouldShowReleaseNotes(manifest,"2.0.0"),false);assert.equal(shouldShowReleaseNotes(empty,"1.0.0"),false)
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
export interface ReleaseCommit {
|
||||
sha:string; shortSha:string; subject:string; author:string; type:string
|
||||
}
|
||||
export interface ReleaseManifest {
|
||||
version:string; tag:string; publishedAt:string|null; releaseUrl:string; commits:ReleaseCommit[]
|
||||
}
|
||||
export function safeReleaseUrl(value:unknown):string|null {
|
||||
if(typeof value!=="string"||!value.trim())return null
|
||||
try{const url=new URL(value.trim());return ["https:","http:"].includes(url.protocol)&&!url.username&&!url.password?url.href:null}catch{return null}
|
||||
}
|
||||
const text=(value:unknown,max:number):value is string=>typeof value==="string"&&value.length>0&&value.length<=max
|
||||
export function parseReleaseManifest(value:unknown):ReleaseManifest|null {
|
||||
if(!value||typeof value!=="object"||Array.isArray(value))return null
|
||||
const row=value as Record<string,unknown>
|
||||
if(!text(row.version,100)||!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(row.version)||row.tag!=="v"+row.version)return null
|
||||
if(row.publishedAt!==null&&(typeof row.publishedAt!=="string"||!/^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(row.publishedAt)||!Number.isFinite(Date.parse(row.publishedAt))))return null
|
||||
if(typeof row.releaseUrl!=="string"||(row.releaseUrl!==""&&!safeReleaseUrl(row.releaseUrl)))return null
|
||||
if(!Array.isArray(row.commits)||row.commits.length>1000)return null
|
||||
const commits:ReleaseCommit[]=[];const ids=new Set<string>()
|
||||
for(const item of row.commits){
|
||||
if(!item||typeof item!=="object"||Array.isArray(item))return null
|
||||
const c=item as Record<string,unknown>
|
||||
if(!text(c.sha,64)||! /^[a-f0-9]{7,64}$/i.test(c.sha)||ids.has(c.sha)||!text(c.shortSha,12)||! /^[a-f0-9]{4,12}$/i.test(c.shortSha)||!c.sha.startsWith(c.shortSha)||!text(c.subject,4000)||!text(c.author,500)||!text(c.type,30))return null
|
||||
ids.add(c.sha);commits.push({sha:c.sha,shortSha:c.shortSha,subject:c.subject,author:c.author,type:c.type})
|
||||
}
|
||||
return {version:row.version,tag:row.tag as string,publishedAt:row.publishedAt as string|null,releaseUrl:row.releaseUrl? safeReleaseUrl(row.releaseUrl)!:"",commits}
|
||||
}
|
||||
|
||||
export function shouldShowReleaseNotes(manifest:ReleaseManifest,currentVersion:string):boolean {
|
||||
return !currentVersion.endsWith("-dev")&&manifest.version===currentVersion&&manifest.commits.length>0
|
||||
}
|
||||
Reference in new issue
Block a user