Publish #151
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| # Trusted publishing (npm): id-token: write lets GitHub Actions mint an OIDC | |
| # token that npm CLI auto-exchanges for a short-lived publish token, so we no | |
| # longer rely on a long-lived NPM_TOKEN. See https://docs.npmjs.com/trusted-publishers | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| jobs: | |
| publish-npm: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| is_prerelease: ${{ steps.meta.outputs.is_prerelease }} | |
| version: ${{ steps.meta.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Determine release metadata | |
| id: meta | |
| run: | | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| if [[ "$VERSION" == *"-"* ]]; then | |
| echo "is_prerelease=true" >> "$GITHUB_OUTPUT" | |
| echo "npm_tag=next" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_prerelease=false" >> "$GITHUB_OUTPUT" | |
| echo "npm_tag=latest" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| registry-url: 'https://registry.npmjs.org' | |
| - run: pnpm install --frozen-lockfile | |
| - name: Build all packages | |
| run: pnpm build | |
| - name: Build Web UI | |
| run: pnpm --filter @markus/web-ui build | |
| - name: Pack Chrome extension | |
| run: node scripts/ensure-chrome-extension-zip.mjs | |
| - name: Bundle CLI for publishing | |
| run: pnpm --filter @markus-global/cli build:bundle | |
| - name: Verify bundle | |
| run: | | |
| ls -lh packages/cli/dist/markus.mjs | |
| ls -d packages/cli/dist/web-ui || { echo "Web UI not bundled"; exit 1; } | |
| ls -d packages/cli/templates || { echo "templates not bundled"; exit 1; } | |
| ls -lh packages/cli/dist/markus-browser-extension.zip | |
| # npm pack dry-run must include the zip | |
| cd packages/cli && npm pack --dry-run 2>&1 | grep -F 'markus-browser-extension.zip' | |
| - name: Upgrade npm CLI (Trusted Publishing needs >= 11.5.1) | |
| run: npm install -g npm@latest | |
| - name: Publish to npm (Trusted Publishing) | |
| run: cd packages/cli && npm publish --access public --provenance --tag ${{ steps.meta.outputs.npm_tag }} | |
| build-server-binary: | |
| needs: publish-npm | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - name: Build all packages | |
| run: pnpm build | |
| - name: Build Web UI | |
| run: pnpm --filter @markus/web-ui build | |
| - name: Pack Chrome extension | |
| run: node scripts/ensure-chrome-extension-zip.mjs | |
| - name: Bundle CLI for publishing | |
| run: pnpm --filter @markus-global/cli build:bundle | |
| - name: Build binary archive | |
| run: bash scripts/build-binary.sh linux x64 | |
| - name: Upload versioned installer | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: markus-linux-x64 | |
| path: | | |
| dist-binary/markus-v*-linux-x64.* | |
| !dist-binary/markus-v*-linux-x64/ | |
| retention-days: 5 | |
| - name: Upload fixed-name installer | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: markus-setup-linux-x64 | |
| path: dist-binary/markus-setup-linux-x64.* | |
| retention-days: 5 | |
| build-desktop: | |
| needs: publish-npm | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-latest | |
| platform: darwin | |
| arch: arm64 | |
| electron_args: "--mac --arm64" | |
| - os: macos-latest | |
| platform: darwin | |
| arch: x64 | |
| electron_args: "--mac --x64" | |
| - os: ubuntu-latest | |
| platform: linux | |
| arch: x64 | |
| electron_args: "--linux --x64" | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - name: Build all packages | |
| run: pnpm build | |
| - name: Build Web UI | |
| run: pnpm --filter @markus/web-ui build | |
| - name: Pack Chrome extension | |
| run: node scripts/ensure-chrome-extension-zip.mjs | |
| - name: Build Electron app | |
| run: pnpm --filter @markus/desktop build:electron | |
| - name: Verify Electron dist includes extension zip | |
| run: ls -lh packages/desktop/dist/markus-browser-extension.zip | |
| - name: Import Apple certificates | |
| if: matrix.platform == 'darwin' | |
| env: | |
| APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| if [ -z "$APPLE_CERTIFICATE_P12" ]; then | |
| echo "⚠ Apple signing secrets not configured — skipping" | |
| exit 0 | |
| fi | |
| KEYCHAIN="desktop-$$.keychain-db" | |
| KEYCHAIN_PASSWORD="$(openssl rand -hex 12)" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" | |
| security set-keychain-settings -lut 900 "$KEYCHAIN" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" | |
| echo "$APPLE_CERTIFICATE_P12" | base64 --decode > /tmp/cert.p12 | |
| security import /tmp/cert.p12 -k "$KEYCHAIN" -P "$APPLE_CERTIFICATE_PASSWORD" \ | |
| -T /usr/bin/codesign | |
| security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" | |
| rm /tmp/cert.p12 | |
| echo "DESKTOP_KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV" | |
| - name: Package Electron app | |
| working-directory: packages/desktop | |
| run: pnpm dist ${{ matrix.electron_args }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| - name: Verify macOS DMG integrity | |
| if: matrix.platform == 'darwin' | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| dmgs=(packages/desktop/dist-electron/*.dmg) | |
| if [ "${#dmgs[@]}" -eq 0 ]; then | |
| echo "::error::No DMG produced under packages/desktop/dist-electron/" | |
| exit 1 | |
| fi | |
| if [ "${#dmgs[@]}" -ne 1 ]; then | |
| echo "::error::Expected exactly one DMG for matrix arch ${{ matrix.arch }}, got:" | |
| printf ' %s\n' "${dmgs[@]}" | |
| exit 1 | |
| fi | |
| dmg="${dmgs[0]}" | |
| echo "→ hdiutil verify $dmg" | |
| hdiutil verify "$dmg" | |
| # Volume names contain spaces (e.g. "Markus 0.9.6-rc.3-arm64"); do not | |
| # parse hdiutil tabular output with awk $NF. Use a private mountroot. | |
| mountroot="$(mktemp -d /tmp/markus-dmg-XXXXXX)" | |
| cleanup() { | |
| # Detach the volume dir (not the mountroot), then remove the empty root. | |
| # Under set -e, a failed rm on a still-mounted RO volume must not fail the job. | |
| if [ -d "$mountroot" ]; then | |
| while IFS= read -r vol; do | |
| hdiutil detach "$vol" -force >/dev/null 2>&1 || true | |
| done < <(find "$mountroot" -mindepth 1 -maxdepth 1 -type d 2>/dev/null || true) | |
| hdiutil detach "$mountroot" -force >/dev/null 2>&1 || true | |
| rm -rf "$mountroot" 2>/dev/null || true | |
| fi | |
| return 0 | |
| } | |
| trap cleanup EXIT | |
| echo "→ hdiutil attach (smoke) $dmg → $mountroot" | |
| hdiutil attach "$dmg" -nobrowse -readonly -mountroot "$mountroot" | |
| app="$(find "$mountroot" -maxdepth 2 -name 'Markus.app' -type d | head -n 1)" | |
| if [ -z "$app" ]; then | |
| echo "::error::Markus.app not found under $mountroot" | |
| find "$mountroot" -maxdepth 3 -print | |
| exit 1 | |
| fi | |
| arch_out="$(lipo -archs "$app/Contents/MacOS/Markus")" | |
| echo " app: $app" | |
| echo " arch: $arch_out" | |
| case "${{ matrix.arch }}" in | |
| arm64) echo "$arch_out" | grep -q arm64 ;; | |
| x64) echo "$arch_out" | grep -q x86_64 ;; | |
| esac | |
| cleanup | |
| trap - EXIT | |
| - name: Cleanup Apple keychain | |
| if: always() && matrix.platform == 'darwin' && env.DESKTOP_KEYCHAIN != '' | |
| run: security delete-keychain "$DESKTOP_KEYCHAIN" 2>/dev/null || true | |
| - name: Upload Electron artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: markus-desktop-${{ matrix.platform }}-${{ matrix.arch }} | |
| path: | | |
| packages/desktop/dist-electron/*.dmg | |
| packages/desktop/dist-electron/*.exe | |
| packages/desktop/dist-electron/*.AppImage | |
| retention-days: 5 | |
| # Windows is built in its own job so the Certum SimplySign signing secrets can | |
| # be gated behind a protected environment (required reviewers) without forcing | |
| # the mac/linux desktop builds through the same manual-approval step. | |
| build-desktop-windows: | |
| needs: publish-npm | |
| runs-on: windows-latest | |
| environment: signing | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - name: Build all packages | |
| run: pnpm build | |
| - name: Build Web UI | |
| run: pnpm --filter @markus/web-ui build | |
| - name: Pack Chrome extension | |
| run: node scripts/ensure-chrome-extension-zip.mjs | |
| - name: Build Electron app | |
| run: pnpm --filter @markus/desktop build:electron | |
| # PowerShell treats `ls -lh` as Get-ChildItem -lh (invalid). Use bash. | |
| - name: Verify Electron dist includes extension zip | |
| shell: bash | |
| run: | | |
| test -f packages/desktop/dist/markus-browser-extension.zip | |
| ls -lh packages/desktop/dist/markus-browser-extension.zip | |
| # Fetch the ssign client used by build/sign.cjs and export its native | |
| # Windows path so electron-builder's custom signer can find it. | |
| # | |
| # Supply-chain: ssign is a small third-party tool that handles our | |
| # long-lived signing credential, so the download is pinned to an exact | |
| # release AND verified against a known SHA-256 before use. If Le-Syl21 | |
| # ever re-tags or the CDN is tampered with, the checksum mismatch aborts | |
| # the build instead of running an unknown binary. Bump both values | |
| # together when upgrading (get the digest from the release assets API). | |
| - name: Install ssign (Certum SimplySign client) | |
| shell: bash | |
| env: | |
| SSIGN_VERSION: v0.1.2 | |
| SSIGN_SHA256: 973b871de89a55dc25aa6384c437bae5f94a5bb04c5e82c91ec01f3cf50deed9 | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL -o ssign.zip \ | |
| "https://github.com/Le-Syl21/ssign/releases/download/${SSIGN_VERSION}/ssign-windows-x86_64.zip" | |
| echo "${SSIGN_SHA256} ssign.zip" | sha256sum -c - | |
| powershell -NoProfile -Command "Expand-Archive -Path ssign.zip -DestinationPath ssign_bin -Force" | |
| SSIGN_MSYS="$(find "$PWD/ssign_bin" -name 'ssign.exe' | head -1)" | |
| if [ -z "$SSIGN_MSYS" ]; then echo "::error::ssign.exe not found in release archive"; exit 1; fi | |
| echo "SSIGN_PATH=$(cygpath -w "$SSIGN_MSYS")" >> "$GITHUB_ENV" | |
| # Full signing (app exe + NSIS installer) happens inside electron-builder | |
| # via build/sign.cjs, so .blockmap / latest.yml are generated from the | |
| # already-signed installer. Signing is skipped (non-fatal) if CERTUM_* are | |
| # unset, so the pipeline still produces an unsigned installer for forks. | |
| - name: Package + sign Electron app (Certum SimplySign) | |
| working-directory: packages/desktop | |
| run: pnpm dist --win --x64 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CERTUM_EMAIL: ${{ secrets.CERTUM_EMAIL }} | |
| CERTUM_OTP: ${{ secrets.CERTUM_OTP }} | |
| - name: Upload Electron artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: markus-desktop-win-x64 | |
| path: packages/desktop/dist-electron/*.exe | |
| retention-days: 5 | |
| github-release: | |
| runs-on: ubuntu-latest | |
| needs: [publish-npm, build-server-binary, build-desktop, build-desktop-windows] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Extract version from tag | |
| id: version | |
| run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: List artifacts | |
| run: ls -lhR artifacts/ || echo "No artifacts" | |
| - name: Create GitHub Release (stable) | |
| if: needs.publish-npm.outputs.is_prerelease == 'false' | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| name: v${{ steps.version.outputs.VERSION }} | |
| prerelease: false | |
| generate_release_notes: true | |
| files: artifacts/* | |
| body: | | |
| ## Desktop App (recommended) | |
| | Platform | Download | | |
| |----------|----------| | |
| | macOS (Apple Silicon) | [Markus.dmg](https://github.com/markus-global/markus/releases/download/v${{ steps.version.outputs.VERSION }}/Markus-${{ steps.version.outputs.VERSION }}-arm64.dmg) | | |
| | macOS (Intel) | [Markus.dmg](https://github.com/markus-global/markus/releases/download/v${{ steps.version.outputs.VERSION }}/Markus-${{ steps.version.outputs.VERSION }}.dmg) | | |
| | Windows x64 | [Markus-Setup.exe](https://github.com/markus-global/markus/releases/download/v${{ steps.version.outputs.VERSION }}/Markus-Setup-${{ steps.version.outputs.VERSION }}.exe) | | |
| | Linux x64 | [Markus.AppImage](https://github.com/markus-global/markus/releases/download/v${{ steps.version.outputs.VERSION }}/Markus-${{ steps.version.outputs.VERSION }}.AppImage) | | |
| ## Server / CLI | |
| ```bash | |
| # macOS / Linux (one-liner) | |
| curl -fsSL https://markus.global/install.sh | bash | |
| # npm (requires Node.js 22+) | |
| npm install -g @markus-global/cli@${{ steps.version.outputs.VERSION }} | |
| ``` | |
| Linux server binary: [markus-setup-linux-x64.deb](https://github.com/markus-global/markus/releases/download/v${{ steps.version.outputs.VERSION }}/markus-setup-linux-x64.deb) | |
| - name: Create GitHub Release (pre-release) | |
| if: needs.publish-npm.outputs.is_prerelease == 'true' | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| name: v${{ steps.version.outputs.VERSION }} (Pre-release) | |
| prerelease: true | |
| generate_release_notes: true | |
| files: artifacts/* | |
| body: | | |
| > **Pre-release** — for testing only. `install.sh` always installs the latest stable. | |
| ```bash | |
| npm install -g @markus-global/cli@${{ steps.version.outputs.VERSION }} | |
| ``` | |
| upload-to-hub: | |
| runs-on: ubuntu-latest | |
| needs: [publish-npm, build-server-binary, build-desktop, build-desktop-windows, github-release] | |
| if: needs.publish-npm.outputs.is_prerelease == 'false' | |
| steps: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Upload to R2 | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com | |
| R2_BUCKET: ${{ secrets.R2_BUCKET_NAME }} | |
| VERSION: ${{ needs.publish-npm.outputs.version }} | |
| run: | | |
| set -e | |
| uploaded=0 | |
| for file in artifacts/markus-setup-* artifacts/markus-v${VERSION}-* artifacts/Markus-*.dmg artifacts/Markus-*.AppImage artifacts/Markus*.exe; do | |
| [ -f "$file" ] || continue | |
| filename=$(basename "$file") | |
| echo "Uploading $filename to R2..." | |
| aws s3 cp "$file" "s3://${R2_BUCKET}/releases/${filename}" \ | |
| --endpoint-url "$R2_ENDPOINT" \ | |
| --content-type "application/octet-stream" | |
| uploaded=$((uploaded + 1)) | |
| done | |
| if [ "$uploaded" -eq 0 ]; then | |
| echo "::error::No artifacts found to upload" | |
| exit 1 | |
| fi | |
| echo "{\"version\":\"$VERSION\",\"updatedAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > /tmp/meta.json | |
| aws s3 cp /tmp/meta.json "s3://${R2_BUCKET}/releases/meta.json" \ | |
| --endpoint-url "$R2_ENDPOINT" \ | |
| --content-type "application/json" | |
| echo "Uploaded $uploaded files + meta.json to R2" |