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
95 changes: 95 additions & 0 deletions .github/workflows/moss-desktop-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Moss Desktop Release

on:
workflow_dispatch:
inputs:
channel:
description: electron-updater channel
required: true
default: latest
type: choice
options:
- latest
- beta

jobs:
macos-release:
name: Build, sign, notarize, and verify macOS artifacts
runs-on: macos-latest
timeout-minutes: 120
env:
APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Download bundled runtimes
run: |
bun run claude:download:all
bun run codex:download:all

- name: Verify runtime and source gates
run: |
bun run test:runtime

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the release scripts before invoking them

In the tree for this commit, package.json does not define test:runtime, release:credentials:strict, test:packaged-app-smoke, release:notarize, or release:evidence:audit, and repo-wide lookup also shows scripts/upload-release.mjs and scripts/verify-release-packaging.mjs are absent. A manual workflow_dispatch therefore exits at this first bun run test:runtime with Script not found before any macOS release artifact can be built, so the workflow needs to commit those scripts or call commands that already exist.

Useful? React with 👍 / 👎.

bun run ts:check --pretty false

- name: Build app
run: bun run build

- name: Verify Apple release credentials
run: bun run release:credentials:strict

- name: Package signed macOS artifacts
run: |
rm -rf release
bun run package:mac

- name: Smoke packaged app bundle
run: bun run test:packaged-app-smoke

- name: Notarize and staple release artifacts
run: bun run release:notarize

- name: Generate update manifests
run: node scripts/generate-update-manifest.mjs --channel "${{ inputs.channel }}"

- name: Prepare release upload plan
run: node scripts/upload-release.mjs --dry-run --channel "${{ inputs.channel }}"

- name: Audit signed release evidence
run: bun run release:evidence:audit --require-notarization

- name: Verify commercial release evidence
run: node scripts/verify-release-packaging.mjs --require-artifacts --require-bundled-binaries --require-notarization --require-upload-plan

- name: Upload verified release artifacts
uses: actions/upload-artifact@v4
with:
name: moss-desktop-macos-${{ inputs.channel }}
path: |
release/*.dmg
release/*.zip
release/*mac*.yml
release/notarization-*.json
release/codesign-*.txt
release/staple-*.txt
release/spctl-*.txt
.1code/program/packaged-app-smoke/**/report.json
.1code/program/release-credentials/**/report.json
.1code/program/release-evidence-audit/**/report.json
.1code/program/release-upload/**/manifest.json
if-no-files-found: error
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)
}
Loading