Skip to content

Nightly

Nightly #74

Workflow file for this run

name: Nightly
on:
schedule:
- cron: '0 3 * * *' # 03:00 UTC daily
workflow_dispatch: {} # manual trigger (always builds)
permissions:
contents: write
jobs:
check:
# Cheap gate so scheduled runs don't cut an empty nightly when nothing
# has landed. Manual dispatches always build.
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.decide.outputs.should_build }}
steps:
- name: Checkout git repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Decide whether to build
id: decide
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "should_build=true" >> "$GITHUB_OUTPUT"
elif [ -n "$(git log --since='24 hours ago' --oneline)" ]; then
echo "should_build=true" >> "$GITHUB_OUTPUT"
else
echo "should_build=false" >> "$GITHUB_OUTPUT"
echo "No commits in the last 24h — skipping nightly."
fi
build:
needs: check
if: needs.check.outputs.should_build == 'true'
runs-on: macos-latest
steps:
- name: Checkout git repo
uses: actions/checkout@v4
- name: Install Node and NPM
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm install
- name: Set nightly version
# Build-time only — not committed. electron-builder packages from
# directories.app (release/app), so the app version it bakes in (and
# what app.getVersion() returns) comes from release/app/package.json —
# NOT apps/desktop/package.json. Bump that one. Each nightly takes the
# next patch above the highest version ever published (release tags +
# the local base), so versions increment cleanly (0.1.2, 0.1.3, …)
# instead of carrying a timestamp suffix.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cd apps/desktop/release/app
gh api "repos/${{ github.repository }}/releases" --paginate --jq '.[].tag_name' > /tmp/release-tags.txt || true
node -e "
const fs = require('fs');
const p = require('./package.json');
const parse = (s) => {
const m = String(s).trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
return m ? [+m[1], +m[2], +m[3]] : null;
};
const tags = fs.existsSync('/tmp/release-tags.txt')
? fs.readFileSync('/tmp/release-tags.txt', 'utf8').split('\n')
: [];
const cmp = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
const max = [...tags, p.version].map(parse).filter(Boolean).sort(cmp).pop();
p.version = [max[0], max[1], max[2] + 1].join('.');
fs.writeFileSync('./package.json', JSON.stringify(p, null, 2) + '\n');
console.log('Building nightly', p.version);
"
- name: Build shared package
run: npm run build -w @talyn/shared
- name: Build desktop app
# These are baked into the renderer bundle at build time by webpack's
# EnvironmentPlugin — they must be set on THIS step, not the publish
# step. Without them the app defaults to http://localhost:4747 with no
# auth config. The Supabase anon key is publishable (client-embedded
# by design), so it lives in repo variables, not secrets.
env:
TALYN_API_URL: https://prod.talyn.dev
TALYN_SUPABASE_URL: ${{ vars.TALYN_SUPABASE_URL }}
TALYN_SUPABASE_ANON_KEY: ${{ vars.TALYN_SUPABASE_ANON_KEY }}
TALYN_POSTHOG_KEY: ${{ vars.TALYN_POSTHOG_KEY }}
TALYN_POSTHOG_HOST: https://us.i.posthog.com
run: npm run build -w @talyn/desktop
- name: Upload renderer source maps to PostHog
# Symbolicates renderer exceptions in PostHog. `inject` stamps chunk IDs
# into the built JS + maps so PostHog can match a minified frame back to
# its source; `upload --delete-after` ships the maps to PostHog then
# deletes them from disk so they aren't packed into the shipped app.
# Runs after the build and before packaging so electron-builder bundles
# the injected JS. No-op until POSTHOG_CLI_API_KEY is configured, and
# never blocks a release — symbolication is auxiliary. (The skip is
# guarded inside the script, not via `if:` — the `secrets` context is
# not allowed in `if` conditions.)
continue-on-error: true
env:
POSTHOG_CLI_HOST: https://us.posthog.com
POSTHOG_CLI_PROJECT_ID: ${{ vars.POSTHOG_CLI_PROJECT_ID }}
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
run: |
if [ -z "$POSTHOG_CLI_API_KEY" ]; then
echo "POSTHOG_CLI_API_KEY not set — skipping source map upload."
exit 0
fi
npx --yes @posthog/cli@latest sourcemap inject \
--directory apps/desktop/release/app/dist/renderer
npx --yes @posthog/cli@latest sourcemap upload \
--directory apps/desktop/release/app/dist/renderer --delete-after
- name: Publish nightly pre-release
env:
# Code-signing + notarization — required or Squirrel.Mac refuses
# to apply the update on the client.
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cd apps/desktop
# electron is hoisted to the monorepo root node_modules, so
# electron-builder can't infer the version from this workspace —
# resolve it explicitly and pass it in.
ELECTRON_VERSION=$(node -p "require('electron/package.json').version")
# arm64-only: every Mac from 2020+ is Apple Silicon, and a single
# notarization submission roughly halves the nightly wait. Intel
# (x64) coverage stays on the stable publish.yml release path.
npm exec electron-builder -- --publish always --arm64 \
-c.electronVersion="$ELECTRON_VERSION" \
-c.publish.releaseType=prerelease