Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"ts:check": "tsgo --noEmit",
"postinstall": "node -e \"if(!process.env.VERCEL){require('child_process').execSync('electron-rebuild -f -w better-sqlite3,node-pty',{stdio:'inherit'})}\" && node scripts/patch-electron-dev.mjs"
"postinstall": "node -e \"if(!process.env.VERCEL){require('child_process').execSync('electron-rebuild -f -w better-sqlite3,node-pty',{stdio:'inherit'})}\" && node scripts/patch-electron-dev.mjs",
"test:runtime": "bun test src/main/lib/moss-account/entitlement.test.ts src/main/lib/moss-source/provider-config.test.ts src/main/lib/mcp-stdio-compat.test.ts src/main/lib/trpc/routers/chat-runtime-selection.test.ts src/main/lib/trpc/routers/codex-mcp-session.test.ts src/main/lib/codex-automations.test.ts src/main/lib/shared-resources/registry.test.ts src/main/lib/shared-resources/codex-native-resources.test.ts src/main/lib/shared-resources/governance.test.ts src/renderer/features/agents/lib/agent-runtime.test.ts src/renderer/features/agents/lib/models.test.ts src/renderer/features/plugins/plugin-entry-surfaces.test.ts src/shared/codex-runtime-notices.test.ts",
"release:credentials:strict": "node scripts/verify-release-credentials.mjs --require-credentials",
"test:packaged-app-smoke": "node scripts/smoke-packaged-app.mjs",
"release:notarize": "node scripts/notarize-release-artifacts.mjs",
"release:evidence:audit": "node scripts/audit-release-evidence.mjs"
},
"dependencies": {
"@ai-sdk/react": "^3.0.14",
Expand Down
257 changes: 257 additions & 0 deletions scripts/audit-release-evidence.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
#!/usr/bin/env node

import fs from "node:fs"
import path from "node:path"

const root = process.cwd()
const requireNotarization = process.argv.includes("--require-notarization")
const generatedAt = new Date().toISOString()
const stamp = generatedAt.replace(/[:.]/g, "-")
const reportDir = path.join(root, ".1code/program/release-evidence-audit", stamp)
const reportPath = path.join(reportDir, "report.json")
const latestPath = path.join(root, ".1code/program/release-evidence-audit/latest.json")
const failures = []
const warnings = []
const blockers = []

function projectPath(relativePath) {
return path.join(root, relativePath)
}

function normalizeRelative(filePath) {
return path.relative(root, filePath).split(path.sep).join("/")
}

function exists(relativePath) {
return fs.existsSync(projectPath(relativePath))
}

function listReleaseFiles() {
const releaseDir = projectPath("release")
if (!fs.existsSync(releaseDir)) return []
return fs.readdirSync(releaseDir)
.map((name) => path.join(releaseDir, name))
.filter((filePath) => fs.statSync(filePath).isFile())
.sort()
}

function releaseAppDirs() {
return [
{
arch: "arm64",
path: "release/mac-arm64/1Code.app",
},
{
arch: "x64",
path: "release/mac/1Code.app",
},
].map((app) => ({
...app,
present: exists(app.path),
}))
}

function readJsonPath(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"))
} catch (error) {
failures.push(`Could not parse ${normalizeRelative(filePath)}: ${error.message}`)
return undefined
}
}

function readTextRelative(relativePath) {
const filePath = projectPath(relativePath)
if (!fs.existsSync(filePath)) return undefined
return fs.readFileSync(filePath, "utf8")
}

function parseNotaryStatus(stdoutPath) {
const raw = readTextRelative(stdoutPath)
if (!raw) return {
path: stdoutPath,
parseable: false,
status: "missing",
}

try {
const parsed = JSON.parse(raw)
return {
path: stdoutPath,
parseable: true,
id: parsed.id ?? null,
status: parsed.status ?? "unknown",
accepted: parsed.status === "Accepted",
}
} catch {
return {
path: stdoutPath,
parseable: false,
status: "unparseable",
}
}
}

function commandOutputReferences(command) {
return [command.stdout, command.stderr]
.filter((value) => typeof value === "string" && value.length > 0)
.map((value) => ({
path: value,
present: exists(value),
}))
}

function inspectNotarizationReport(filePath) {
const report = readJsonPath(filePath)
const commands = Array.isArray(report?.commands) ? report.commands : []
const commandFailures = commands.filter((command) => command.exitCode !== 0)
const references = commands.flatMap(commandOutputReferences)
const missingReferences = references.filter((reference) => !reference.present)
const notaryCommands = commands.filter((command) => String(command.label ?? "").startsWith("notarytool submit"))
const notaryStatuses = notaryCommands
.map((command) => command.stdout)
.filter((stdoutPath) => typeof stdoutPath === "string" && stdoutPath.length > 0)
.map(parseNotaryStatus)
const unacceptedStatuses = notaryStatuses.filter((status) => status.accepted !== true)
const summary = report?.summary ?? {}
const dryRun = report?.mode?.dryRun === true
const valid = Boolean(report)
&& report.status === "passed"
&& dryRun === false
&& commands.length > 0
&& commandFailures.length === 0
&& missingReferences.length === 0
&& Number(summary.notarytoolSubmissions ?? 0) > 0
&& Number(summary.stapleCommands ?? 0) > 0
&& Number(summary.codesignVerifications ?? 0) > 0
&& Number(summary.spctlAssessments ?? 0) > 0
&& notaryStatuses.length > 0
&& unacceptedStatuses.length === 0

return {
path: normalizeRelative(filePath),
status: report?.status ?? "missing",
dryRun,
commandCount: commands.length,
commandFailures: commandFailures.map((command) => ({
label: command.label ?? command.command ?? "unknown",
exitCode: command.exitCode ?? null,
})),
missingReferences,
summary: {
notarytoolSubmissions: Number(summary.notarytoolSubmissions ?? 0),
stapleCommands: Number(summary.stapleCommands ?? 0),
codesignVerifications: Number(summary.codesignVerifications ?? 0),
spctlAssessments: Number(summary.spctlAssessments ?? 0),
},
notaryStatuses,
valid,
}
}

const releaseFiles = listReleaseFiles()
const macArtifacts = releaseFiles
.filter((filePath) => /\.(dmg|zip)$/i.test(filePath))
.map(normalizeRelative)
const updateManifests = releaseFiles
.filter((filePath) => /(?:latest|beta)-mac(?:-x64)?\.yml$/.test(path.basename(filePath)))
.map(normalizeRelative)
const notarizationReportFiles = releaseFiles
.filter((filePath) => /^notarization-.+\.json$/i.test(path.basename(filePath)))
const notarizationReports = notarizationReportFiles.map(inspectNotarizationReport)
const validNotarizationReports = notarizationReports.filter((report) => report.valid)
const apps = releaseAppDirs()
const presentApps = apps.filter((app) => app.present)
const notarizationEvidenceFiles = releaseFiles
.filter((filePath) => /notary|notar|codesign|staple|spctl/i.test(path.basename(filePath)))
.map(normalizeRelative)

if (macArtifacts.length < 4) {
blockers.push(`Expected at least 4 macOS DMG/ZIP artifacts, found ${macArtifacts.length}.`)
}
if (updateManifests.length < 2) {
blockers.push(`Expected at least 2 macOS update manifests, found ${updateManifests.length}.`)
}
if (presentApps.length < 2) {
blockers.push(`Expected both packaged app directories, found ${presentApps.length}.`)
}
if (validNotarizationReports.length === 0) {
blockers.push("No valid signed/notarized release evidence report was found.")
}

for (const report of notarizationReports) {
if (!report.valid) {
warnings.push(`Notarization report ${report.path} is not valid distributable evidence.`)
}
}

if (requireNotarization && blockers.length > 0) {
failures.push(...blockers)
}

const status = failures.length > 0
? "failed"
: blockers.length > 0
? "blocked"
: "passed"

const report = {
status,
generatedAt,
mode: {
requireNotarization,
},
releaseDir: "release",
artifacts: {
macArtifacts,
updateManifests,
notarizationEvidenceFiles,
apps,
},
notarization: {
reports: notarizationReports,
validReports: validNotarizationReports.map((entry) => entry.path),
acceptedSubmissions: notarizationReports.reduce(
(count, entry) => count + entry.notaryStatuses.filter((status) => status.accepted === true).length,
0,
),
validReportCount: validNotarizationReports.length,
},
distribution: {
distributable: status === "passed",
blockerCount: blockers.length,
blockers,
},
warnings,
failures,
}

fs.mkdirSync(reportDir, { recursive: true })
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`)
fs.writeFileSync(latestPath, `${JSON.stringify({
report: normalizeRelative(reportPath),
generatedAt,
status,
}, null, 2)}\n`)

console.log("Moss release evidence audit")
console.log(`status: ${status}`)
console.log(`report: ${normalizeRelative(reportPath)}`)
console.log(`mac artifacts: ${macArtifacts.length}`)
console.log(`update manifests: ${updateManifests.length}`)
console.log(`notarization reports: ${notarizationReports.length}`)
console.log(`valid notarization reports: ${validNotarizationReports.length}`)

for (const message of warnings) {
console.warn(`warning: ${message}`)
}
for (const message of blockers) {
console.warn(`blocker: ${message}`)
}
for (const message of failures) {
console.error(`error: ${message}`)
}

if (failures.length > 0) {
process.exit(1)
}
16 changes: 8 additions & 8 deletions scripts/generate-update-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
* node scripts/generate-update-manifest.mjs
*
* The script expects ZIP files to exist in the release/ directory:
* - Agents-{version}-arm64-mac.zip
* - Agents-{version}-mac.zip
* - 1Code-{version}-arm64-mac.zip
* - 1Code-{version}-mac.zip
*
* Run this after `npm run dist` to generate the manifest files.
*/
Expand Down Expand Up @@ -77,8 +77,8 @@ function findReleaseFile(pattern, ext = ".zip") {
*/
function generateManifest(arch) {
// electron-builder names files differently:
// arm64: Agents-{version}-arm64-mac.zip
// x64: Agents-{version}-mac.zip
// arm64: 1Code-{version}-arm64-mac.zip
// x64: 1Code-{version}-mac.zip
const pattern = arch === "arm64" ? `${version}-arm64-mac` : `${version}-mac`
const zipPath = findReleaseFile(pattern, ".zip")

Expand Down Expand Up @@ -248,13 +248,13 @@ console.log("Next steps:")
console.log("1. Upload the following files to cdn.21st.dev/releases/desktop/:")
if (arm64Manifest) {
console.log(` - ${prefix}-mac.yml`)
console.log(` - Agents-${version}-arm64-mac.zip`)
console.log(` - Agents-${version}-arm64.dmg (for manual download)`)
console.log(` - 1Code-${version}-arm64-mac.zip`)
console.log(` - 1Code-${version}-arm64.dmg (for manual download)`)
}
if (x64Manifest) {
console.log(` - ${prefix}-mac-x64.yml`)
console.log(` - Agents-${version}-mac.zip`)
console.log(` - Agents-${version}.dmg (for manual download)`)
console.log(` - 1Code-${version}-mac.zip`)
console.log(` - 1Code-${version}.dmg (for manual download)`)
}
console.log("2. Create a release entry in the admin dashboard")
console.log("=".repeat(50))
Loading