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

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Existing release tag"
required: true
default: "v1.0.0"

permissions:
contents: write

env:
VERSION: ${{ inputs.version || github.ref_name }}

jobs:
build:
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- platform: macos
os: macos-latest
script: package:mac
artifact: desktop-macos
- platform: windows
os: windows-latest
script: package:win
artifact: desktop-windows
- platform: linux
os: ubuntu-latest
script: package:linux
artifact: desktop-linux

steps:
- uses: actions/checkout@v5
with:
ref: ${{ env.VERSION }}

- uses: actions/setup-node@v5
with:
node-version: 20.19.0
cache: npm

- name: Install Linux packaging dependencies
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y libfuse2

- run: npm ci

- name: Write Apple notarization API key
if: matrix.platform == 'macos'
shell: bash
env:
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: |
if [ -z "${CSC_LINK:-}" ] ||
[ -z "${CSC_KEY_PASSWORD:-}" ] ||
[ -z "${APPLE_API_KEY_BASE64:-}" ] ||
[ -z "${APPLE_API_KEY_ID:-}" ] ||
[ -z "${APPLE_API_ISSUER:-}" ]; then
echo "Skipping macOS notarization setup because signing secrets are incomplete."
exit 0
fi

api_key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
printf "%s" "$APPLE_API_KEY_BASE64" | base64 --decode > "$api_key_path"
chmod 600 "$api_key_path"
echo "APPLE_API_KEY=$api_key_path" >> "$GITHUB_ENV"
echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV"
echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" >> "$GITHUB_ENV"

- name: Build desktop artifacts
env:
CSC_LINK: ${{ matrix.platform == 'macos' && secrets.MAC_CSC_LINK || '' }}
CSC_KEY_PASSWORD: ${{ matrix.platform == 'macos' && secrets.MAC_CSC_KEY_PASSWORD || '' }}
WIN_CSC_LINK: ${{ matrix.platform == 'windows' && secrets.WIN_CSC_LINK || '' }}
WIN_CSC_KEY_PASSWORD: ${{ matrix.platform == 'windows' && secrets.WIN_CSC_KEY_PASSWORD || '' }}
run: npm run ${{ matrix.script }} --workspace @specdock/desktop

- name: Verify macOS signature
if: matrix.platform == 'macos'
shell: bash
env:
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
run: |
if [ -z "${CSC_LINK:-}" ]; then
echo "Skipping macOS signature verification because MAC_CSC_LINK is not configured."
exit 0
fi
app_path="$(find apps/desktop/release/desktop -name "SpecDock.app" -type d | head -n 1)"
test -n "$app_path"
codesign --verify --deep --strict --verbose=2 "$app_path"
spctl --assess --type execute --verbose "$app_path"

- name: Verify Windows signatures
if: matrix.platform == 'windows'
shell: pwsh
env:
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
run: |
if (-not $env:WIN_CSC_LINK) {
Write-Host "Skipping Windows signature verification because WIN_CSC_LINK is not configured."
exit 0
}
Get-ChildItem apps/desktop/release/desktop -Recurse -File -Filter *.exe |
ForEach-Object {
$signature = Get-AuthenticodeSignature $_.FullName
if ($signature.Status -ne "Valid") {
throw "Invalid signature for $($_.FullName): $($signature.Status)"
}
}

- name: Collect desktop artifacts
if: matrix.platform != 'windows'
shell: bash
run: |
mkdir -p release-upload
find apps/desktop/release/desktop -maxdepth 1 -type f \( \
-name "*.dmg" -o \
-name "*.zip" -o \
-name "*.AppImage" -o \
-name "*.tar.gz" -o \
-name "*.blockmap" -o \
-name "latest*.yml" \
\) -exec cp {} release-upload/ \;

- name: Collect desktop artifacts
if: matrix.platform == 'windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force release-upload
Get-ChildItem apps/desktop/release/desktop -File |
Where-Object {
$_.Extension -in ".exe", ".zip", ".blockmap", ".yml" -or
$_.Name -like "latest*.yml"
} |
Copy-Item -Destination release-upload

- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: release-upload/*
if-no-files-found: error

publish:
name: Publish GitHub release
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v5
with:
ref: ${{ env.VERSION }}

- uses: actions/setup-node@v5
with:
node-version: 20.19.0
cache: npm

- uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true

- name: Generate checksums
run: npm run release:checksums -- release-assets

- name: Create or update release
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "$VERSION" >/dev/null 2>&1; then
gh release upload "$VERSION" release-assets/* --clobber
else
gh release create "$VERSION" \
--verify-tag \
--title "SpecDock $VERSION" \
--notes-file "docs/release-notes/${VERSION}.md" \
release-assets/*
fi
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
version:
description: "Docker image version tag"
required: true
default: "v0.2.3"
default: "v1.0.0"

env:
DOCKERHUB_IMAGE: docker.io/d8vik/specdock
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
node_modules
dist
build
!apps/desktop/build/
!apps/desktop/build/**
coverage
.history
.env
Expand All @@ -15,3 +17,6 @@ coverage
docs_deprecated
docs/BOOTSTRAP_REPOSITORY.md
docs/TASKS.md
apps/desktop/release
apps/api/src/**/*.js
apps/api/src/**/*.js.map
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
COPY . .
RUN npm run build
RUN npm run build --workspace @specdock/core \
&& npm run build --workspace @specdock/generator \
&& npm run build --workspace @specdock/ui \
&& npm run build --workspace @specdock/api \
&& npm run build --workspace @specdock/web

FROM node:20.19-alpine AS runner
WORKDIR /app
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ docker run -d --name specdock \
-p 127.0.0.1:3000:3000 \
-e PUBLIC_DEMO=true \
-e PROXY_ENABLED=false \
docker.io/d8vik/specdock:v0.5.0
docker.io/d8vik/specdock:v1.0.0
```

Or keep configuration in a local env file:
Expand All @@ -96,7 +96,7 @@ MOCK_SERVER_ENABLED=false
docker run -d --name specdock \
-p 127.0.0.1:3000:3000 \
--env-file ./specdock.env \
docker.io/d8vik/specdock:v0.5.0
docker.io/d8vik/specdock:v1.0.0
```

If you prefer Compose with the published image, create your own
Expand All @@ -105,7 +105,7 @@ If you prefer Compose with the published image, create your own
```yaml
services:
specdock:
image: docker.io/d8vik/specdock:v0.5.0
image: docker.io/d8vik/specdock:v1.0.0
ports:
- "127.0.0.1:3000:3000"
environment:
Expand All @@ -125,7 +125,7 @@ Check health:
curl -fsS http://127.0.0.1:3000/api/health
```

Use immutable version tags such as `docker.io/d8vik/specdock:v0.5.0`.
Use immutable version tags such as `docker.io/d8vik/specdock:v1.0.0`.
The project does not rely on `latest` for releases.

## Configuration
Expand Down
6 changes: 3 additions & 3 deletions apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@specdock/api",
"version": "0.5.0",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -12,8 +12,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@specdock/core": "0.5.0",
"@specdock/generator": "0.5.0",
"@specdock/core": "1.0.0",
"@specdock/generator": "1.0.0",
"fastify": "5.8.5",
"tsx": "4.22.4"
},
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export const generationErrorCode = (error: unknown): string => {
return "GENERATION_TOO_COMPLEX";
}

if (error.message.includes("Generated file path")) {
return "VALIDATION_ERROR";
}

if (error.message.includes("Specification")) {
return "INVALID_SPEC";
}
Expand Down
19 changes: 18 additions & 1 deletion apps/api/src/generate-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ describe("generate routes", () => {
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({
files: [{ path: "generated/client.ts" }],
meta: { fileCount: 1 }
meta: {
fileCount: 1,
outputPlan: {
outputRoot: "generated",
fileCount: 1,
files: [{ path: "generated/client.ts", relativePath: "client.ts" }]
}
}
});
});

Expand Down Expand Up @@ -138,6 +145,16 @@ describe("generate routes", () => {
expect(errorCode(response)).toBe("GENERATED_OUTPUT_TOO_LARGE");
});

it("rejects generated files that escape the output root", async () => {
const response = await injectGenerate(async () => ({
kind: "files",
files: [{ path: "generated/../secret.ts", content: "" }]
}));

expect(response.statusCode).toBe(400);
expect(errorCode(response)).toBe("VALIDATION_ERROR");
});

it("maps child stdout size errors", async () => {
const response = await injectGenerate(async () => {
throw new GeneratedOutputTooLargeError();
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/generate-routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { FastifyInstance } from "fastify";
import { LIMITS, type GenerateRequest } from "@specdock/core";
import { GENERATOR_VERSION } from "@specdock/generator";
import { createGeneratedOutputPlan, GENERATOR_VERSION } from "@specdock/generator";
import { generationErrorCode, sendError } from "./errors.js";
import { resolveGenerateOptions } from "./generation.js";
import {
Expand Down Expand Up @@ -59,7 +59,8 @@ export const registerGenerateRoutes = (
meta: {
fileCount: result.files.length,
generatedAt: new Date().toISOString(),
generatorVersion: GENERATOR_VERSION
generatorVersion: GENERATOR_VERSION,
outputPlan: createGeneratedOutputPlan(result.files, resolvedOptions)
}
};
} catch (error) {
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop/build/entitlements.mac.inherit.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
13 changes: 13 additions & 0 deletions apps/desktop/build/entitlements.mac.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
Binary file added apps/desktop/build/icon.icns
Binary file not shown.
Binary file added apps/desktop/build/icon.ico
Binary file not shown.
Binary file added apps/desktop/build/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions apps/desktop/build/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/desktop/build/icons/128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/desktop/build/icons/16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/desktop/build/icons/256x256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/desktop/build/icons/32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/desktop/build/icons/512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/desktop/build/icons/64x64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading