Skip to content

Commit ff44f07

Browse files
committed
move auto-update feed to s3
1 parent e6485ac commit ff44f07

19 files changed

Lines changed: 320 additions & 149 deletions

File tree

.github/workflows/code-release.yml

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,25 @@ jobs:
233233
apps/code/out/*-mac.zip \
234234
apps/code/out/*.blockmap
235235
236+
- name: Configure AWS credentials for the update feed
237+
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
238+
with:
239+
role-to-assume: ${{ secrets.AWS_DESKTOP_APP_RELEASES_ROLE_ARN }}
240+
aws-region: us-east-1
241+
mask-aws-account-id: true
242+
unset-current-credentials: true
243+
244+
# Binaries carry the version in their filename, so they are immutable.
245+
# Channel files are uploaded last by finalize-release; updaters only see
246+
# this release once those land.
247+
- name: Upload artifacts to the S3 update feed
248+
working-directory: apps/code/out
249+
run: |
250+
for f in *.dmg *-mac.zip *.blockmap; do
251+
aws s3 cp "$f" "s3://posthog-desktop-app-releases-prod-us/stable/$f" \
252+
--cache-control "public, max-age=31536000, immutable"
253+
done
254+
236255
# Hash the same files uploaded above; finalize-release turns these into
237256
# the download tables in the release notes.
238257
- name: Compute artifact checksums
@@ -357,6 +376,35 @@ jobs:
357376
$files = $items | Select-Object -ExpandProperty FullName
358377
gh release upload "v$env:APP_VERSION" --repo PostHog/code --clobber @files
359378
379+
- name: Configure AWS credentials for the update feed
380+
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
381+
with:
382+
role-to-assume: ${{ secrets.AWS_DESKTOP_APP_RELEASES_ROLE_ARN }}
383+
aws-region: us-east-1
384+
mask-aws-account-id: true
385+
unset-current-credentials: true
386+
387+
# latest.yml is NOT uploaded here: finalize-release injects release notes
388+
# and publishes it last, which is what flips the S3 feed to this version.
389+
- name: Upload artifacts to the S3 update feed
390+
shell: pwsh
391+
run: |
392+
$out = "apps/code/out"
393+
$items = @()
394+
$items += Get-ChildItem $out -File -Filter "*.exe"
395+
$items += Get-ChildItem $out -File -Filter "*.blockmap"
396+
foreach ($f in $items) {
397+
aws s3 cp $f.FullName "s3://posthog-desktop-app-releases-prod-us/stable/$($f.Name)" --cache-control "public, max-age=31536000, immutable"
398+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
399+
}
400+
401+
- name: Upload windows manifest artifact
402+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
403+
with:
404+
name: win-manifest
405+
path: apps/code/out/latest.yml
406+
retention-days: 1
407+
360408
# Hash the same files uploaded above (minus latest.yml); finalize-release
361409
# turns these into the download tables in the release notes.
362410
- name: Compute artifact checksums
@@ -483,6 +531,30 @@ jobs:
483531
apps/code/out/*.deb \
484532
apps/code/out/*.rpm
485533
534+
- name: Configure AWS credentials for the update feed
535+
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
536+
with:
537+
role-to-assume: ${{ secrets.AWS_DESKTOP_APP_RELEASES_ROLE_ARN }}
538+
aws-region: us-east-1
539+
mask-aws-account-id: true
540+
unset-current-credentials: true
541+
542+
# No Linux auto-updater consumes latest-linux*.yml today (updates are
543+
# macOS and Windows only), so the manifests can ship with the binaries
544+
# instead of waiting for the finalize flip.
545+
- name: Upload artifacts to the S3 update feed
546+
working-directory: apps/code/out
547+
run: |
548+
for f in *.AppImage *.deb *.rpm; do
549+
aws s3 cp "$f" "s3://posthog-desktop-app-releases-prod-us/stable/$f" \
550+
--cache-control "public, max-age=31536000, immutable"
551+
done
552+
for f in latest-linux*.yml; do
553+
[ -e "$f" ] || continue
554+
aws s3 cp "$f" "s3://posthog-desktop-app-releases-prod-us/stable/$f" \
555+
--cache-control "no-cache"
556+
done
557+
486558
# Hash the same files uploaded above; finalize-release turns these into
487559
# the download tables in the release notes.
488560
- name: Compute artifact checksums
@@ -506,6 +578,7 @@ jobs:
506578
runs-on: ubuntu-latest
507579
permissions:
508580
contents: write
581+
id-token: write
509582
steps:
510583
- name: Get app token
511584
id: app-token
@@ -520,12 +593,14 @@ jobs:
520593
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
521594
echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
522595
523-
- name: Checkout merge script
596+
- name: Checkout release scripts
524597
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
525598
with:
526599
sparse-checkout: |
527600
apps/code/scripts/merge-mac-manifests.mjs
528601
apps/code/scripts/generate-release-download-tables.mjs
602+
apps/code/scripts/build-releases-feed.mjs
603+
apps/code/scripts/inject-release-notes.mjs
529604
sparse-checkout-cone-mode: false
530605

531606
- name: Setup Node.js
@@ -579,3 +654,42 @@ jobs:
579654
printf '\n' >> /tmp/release-notes.md
580655
node apps/code/scripts/generate-release-download-tables.mjs "$APP_VERSION" /tmp/checksums >> /tmp/release-notes.md
581656
gh release edit "v$APP_VERSION" --repo PostHog/code --draft=false --notes-file /tmp/release-notes.md
657+
658+
- name: Download windows manifest
659+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
660+
with:
661+
name: win-manifest
662+
path: /tmp/win-manifest
663+
664+
# Built after the GitHub release is published so the new release (and its
665+
# generated notes) is included; the in-app release notes read this file.
666+
- name: Build releases feed manifest
667+
env:
668+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
669+
run: node apps/code/scripts/build-releases-feed.mjs /tmp/releases.json
670+
671+
# The generic provider fetches nothing from GitHub, so the channel files
672+
# must carry the notes for electron-updater's UpdateInfo.releaseNotes.
673+
- name: Inject release notes into channel files
674+
run: |
675+
node apps/code/scripts/inject-release-notes.mjs /tmp/release-notes.md \
676+
/tmp/latest-mac.yml /tmp/win-manifest/latest.yml
677+
678+
- name: Configure AWS credentials for the update feed
679+
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
680+
with:
681+
role-to-assume: ${{ secrets.AWS_DESKTOP_APP_RELEASES_ROLE_ARN }}
682+
aws-region: us-east-1
683+
mask-aws-account-id: true
684+
unset-current-credentials: true
685+
686+
# The channel-file upload is the S3 publish flip: updaters only see the
687+
# new version once these land, after every platform's binaries are up.
688+
- name: Publish channel files to the S3 update feed
689+
run: |
690+
aws s3 cp /tmp/releases.json "s3://posthog-desktop-app-releases-prod-us/stable/releases.json" \
691+
--cache-control "no-cache"
692+
aws s3 cp /tmp/latest-mac.yml "s3://posthog-desktop-app-releases-prod-us/stable/latest-mac.yml" \
693+
--cache-control "no-cache"
694+
aws s3 cp /tmp/win-manifest/latest.yml "s3://posthog-desktop-app-releases-prod-us/stable/latest.yml" \
695+
--cache-control "no-cache"

apps/code/electron-builder.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,12 @@ const config: Configuration = {
130130
packageName: "posthog-code",
131131
},
132132

133+
// Installs built from this config poll the S3 feed. Installs built before
134+
// the feed moved poll GitHub Releases on PostHog/code, so CI dual-publishes
135+
// there until that fleet drains.
133136
publish: {
134-
provider: "github",
135-
owner: "PostHog",
136-
repo: "code",
137-
releaseType: "draft",
137+
provider: "generic",
138+
url: "https://posthog-desktop-app-releases-prod-us.s3.us-east-1.amazonaws.com/stable",
138139
},
139140
};
140141

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env node
2+
import { realpathSync, writeFileSync } from "node:fs";
3+
import { fileURLToPath } from "node:url";
4+
5+
const RELEASES_API_URL =
6+
"https://api.github.com/repos/PostHog/code/releases?per_page=30";
7+
8+
export function toFeedReleases(apiReleases) {
9+
return apiReleases
10+
.filter((release) => !release.draft)
11+
.map((release) => ({
12+
version: release.tag_name.replace(/^v/, ""),
13+
name:
14+
release.name && release.name.length > 0
15+
? release.name
16+
: release.tag_name,
17+
notes: release.body ?? "",
18+
date: release.published_at,
19+
isPrerelease: release.prerelease,
20+
htmlUrl: release.html_url,
21+
}));
22+
}
23+
24+
async function main() {
25+
const [, , outputPath] = process.argv;
26+
27+
if (!outputPath) {
28+
console.error("Usage: build-releases-feed.mjs <output-json>");
29+
process.exit(1);
30+
}
31+
32+
const headers = { Accept: "application/vnd.github+json" };
33+
if (process.env.GH_TOKEN) {
34+
headers.Authorization = `Bearer ${process.env.GH_TOKEN}`;
35+
}
36+
37+
const response = await fetch(RELEASES_API_URL, { headers });
38+
if (!response.ok) {
39+
throw new Error(`GitHub releases fetch failed: ${response.status}`);
40+
}
41+
42+
const releases = toFeedReleases(await response.json());
43+
writeFileSync(
44+
outputPath,
45+
`${JSON.stringify({ releases }, null, 2)}\n`,
46+
"utf8",
47+
);
48+
console.log(`Wrote ${releases.length} releases -> ${outputPath}`);
49+
}
50+
51+
if (
52+
process.argv[1] &&
53+
realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
54+
) {
55+
await main();
56+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env node
2+
import { readFileSync, realpathSync, writeFileSync } from "node:fs";
3+
import { fileURLToPath } from "node:url";
4+
import { parse, stringify } from "yaml";
5+
6+
export function withReleaseNotes(manifest, notes) {
7+
return { ...manifest, releaseNotes: notes };
8+
}
9+
10+
function main() {
11+
const [, , notesPath, ...manifestPaths] = process.argv;
12+
13+
if (!notesPath || manifestPaths.length === 0) {
14+
console.error(
15+
"Usage: inject-release-notes.mjs <notes-md> <channel-yml> [...channel-yml]",
16+
);
17+
process.exit(1);
18+
}
19+
20+
const notes = readFileSync(notesPath, "utf8").trim();
21+
for (const manifestPath of manifestPaths) {
22+
const manifest = parse(readFileSync(manifestPath, "utf8"));
23+
writeFileSync(
24+
manifestPath,
25+
stringify(withReleaseNotes(manifest, notes)),
26+
"utf8",
27+
);
28+
console.log(`Injected release notes -> ${manifestPath}`);
29+
}
30+
}
31+
32+
if (
33+
process.argv[1] &&
34+
realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
35+
) {
36+
main();
37+
}

apps/code/src/main/di/container.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ import type { ExternalAppsPreferences } from "@posthog/workspace-server/services
169169
import { foldersModule } from "@posthog/workspace-server/services/folders/folders.module";
170170
import { GitService } from "@posthog/workspace-server/services/git/service";
171171
import { TaskPrStatusService } from "@posthog/workspace-server/services/git/task-pr-status";
172-
import { githubReleasesModule } from "@posthog/workspace-server/services/github-releases/github-releases.module";
173172
import {
174173
HANDOFF_GIT_GATEWAY,
175174
HANDOFF_LOG_GATEWAY,
@@ -188,6 +187,7 @@ import { POSTHOG_PLUGIN_SERVICE } from "@posthog/workspace-server/services/posth
188187
import { posthogPluginModule } from "@posthog/workspace-server/services/posthog-plugin/posthog-plugin.module";
189188
import { PROCESS_TRACKING_SERVICE } from "@posthog/workspace-server/services/process-tracking/identifiers";
190189
import { processTrackingModule } from "@posthog/workspace-server/services/process-tracking/process-tracking.module";
190+
import { releaseFeedModule } from "@posthog/workspace-server/services/release-feed/release-feed.module";
191191
import { SECURE_STORE_SERVICE } from "@posthog/workspace-server/services/secure-store/identifiers";
192192
import { shellModule } from "@posthog/workspace-server/services/shell/shell.module";
193193
import { skillsModule } from "@posthog/workspace-server/services/skills/skills.module";
@@ -618,7 +618,7 @@ container.load(posthogPluginModule);
618618
container.bind(MAIN_POSTHOG_PLUGIN_SERVICE).toService(POSTHOG_PLUGIN_SERVICE);
619619
container.load(skillsModule);
620620
container.load(skillsMarketplaceModule);
621-
container.load(githubReleasesModule);
621+
container.load(releaseFeedModule);
622622
container.load(onboardingImportModule);
623623
container.load(claudeCliSessionsModule);
624624
container.load(additionalDirectoriesModule);

apps/code/src/main/trpc/router.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { foldersRouter } from "@posthog/host-router/routers/folders.router";
2424
import { fsRouter } from "@posthog/host-router/routers/fs.router";
2525
import { gitRouter } from "@posthog/host-router/routers/git.router";
2626
import { githubIntegrationRouter } from "@posthog/host-router/routers/github-integration.router";
27-
import { githubReleasesRouter } from "@posthog/host-router/routers/github-releases.router";
2827
import { handoffRouter } from "@posthog/host-router/routers/handoff.router";
2928
import { linearIntegrationRouter } from "@posthog/host-router/routers/linear-integration.router";
3029
import { llmGatewayRouter } from "@posthog/host-router/routers/llm-gateway.router";
@@ -37,6 +36,7 @@ import { onboardingImportRouter } from "@posthog/host-router/routers/onboarding-
3736
import { osRouter } from "@posthog/host-router/routers/os.router";
3837
import { processTrackingRouter } from "@posthog/host-router/routers/process-tracking.router";
3938
import { provisioningRouter } from "@posthog/host-router/routers/provisioning.router";
39+
import { releaseFeedRouter } from "@posthog/host-router/routers/release-feed.router";
4040
import { secureStoreRouter } from "@posthog/host-router/routers/secure-store.router";
4141
import { shellRouter } from "@posthog/host-router/routers/shell.router";
4242
import { skillsRouter } from "@posthog/host-router/routers/skills.router";
@@ -82,7 +82,7 @@ export const trpcRouter = router({
8282
fs: fsRouter,
8383
git: gitRouter,
8484
githubIntegration: githubIntegrationRouter,
85-
githubReleases: githubReleasesRouter,
85+
releaseFeed: releaseFeedRouter,
8686
handoff: handoffRouter,
8787
linearIntegration: linearIntegrationRouter,
8888
llmGateway: llmGatewayRouter,

docs/UPDATES.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ The version in `apps/code/package.json` is set to `0.0.0-dev` - this is intentio
1313

1414
## Auto-Update Mechanism
1515

16-
PostHog Code uses [electron-updater](https://www.electron.build/auto-update) (the npm package, not the built-in Electron autoUpdater). On startup the app checks for updates against the GitHub Release for PostHog/code.
16+
PostHog Code uses [electron-updater](https://www.electron.build/auto-update) (the npm package, not the built-in Electron autoUpdater) with the generic provider. On startup the app checks for updates against the S3 feed at `https://posthog-desktop-app-releases-prod-us.s3.us-east-1.amazonaws.com/stable`, baked into `app-update.yml` inside the app bundle at package time.
1717

18-
electron-builder generates and uploads a `latest-mac.yml` (macOS) or `latest.yml` (Windows) manifest to the GitHub Release during CI. The path to these manifests is baked into `app-update.yml` inside the app bundle at package time.
18+
Release CI uploads the binaries and blockmaps to the feed from each platform job, then the finalize job uploads the channel files (`latest-mac.yml`, `latest.yml`) last. Updaters only see a release once the channel files change, so that upload is the publish step. The finalize job also injects the generated release notes into the channel files (the generic provider fetches nothing from GitHub, so `UpdateInfo.releaseNotes` comes from the manifest) and publishes `releases.json`, which powers the in-app release notes and What's New history.
19+
20+
**Dual publish**: installs built before the feed moved to S3 poll GitHub Releases on PostHog/code, so CI keeps uploading the same artifacts and manifests there until that fleet drains. The GitHub release also remains the human-facing changelog and download page.
1921

2022
**macOS**: DMG + zip artifacts are uploaded; the merged `latest-mac.yml` covers both arm64 and x64 so the correct build is selected per architecture.
2123

2224
**Windows**: A single NSIS installer is shipped and updated through electron-updater via `latest.yml`. The legacy Squirrel.Windows installer is no longer built; anyone still on an old Squirrel install must reinstall once via the NSIS installer to keep receiving updates.
2325

24-
**Linux**: No auto-update. AppImage, deb and rpm packages are manual downloads from the GitHub Release.
26+
**Linux**: No auto-update. AppImage, deb and rpm packages are manual downloads from the GitHub Release, also mirrored to the S3 feed.
2527

2628
## How It Works
2729

packages/host-router/src/router.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { foldersRouter } from "./routers/folders.router";
2323
import { fsRouter } from "./routers/fs.router";
2424
import { gitRouter } from "./routers/git.router";
2525
import { githubIntegrationRouter } from "./routers/github-integration.router";
26-
import { githubReleasesRouter } from "./routers/github-releases.router";
2726
import { handoffRouter } from "./routers/handoff.router";
2827
import { linearIntegrationRouter } from "./routers/linear-integration.router";
2928
import { llmGatewayRouter } from "./routers/llm-gateway.router";
@@ -36,6 +35,7 @@ import { onboardingImportRouter } from "./routers/onboarding-import.router";
3635
import { osRouter } from "./routers/os.router";
3736
import { processTrackingRouter } from "./routers/process-tracking.router";
3837
import { provisioningRouter } from "./routers/provisioning.router";
38+
import { releaseFeedRouter } from "./routers/release-feed.router";
3939
import { secureStoreRouter } from "./routers/secure-store.router";
4040
import { shellRouter } from "./routers/shell.router";
4141
import { skillsRouter } from "./routers/skills.router";
@@ -74,7 +74,7 @@ export const hostRouter = router({
7474
git: gitRouter,
7575
handoff: handoffRouter,
7676
githubIntegration: githubIntegrationRouter,
77-
githubReleases: githubReleasesRouter,
77+
releaseFeed: releaseFeedRouter,
7878
linearIntegration: linearIntegrationRouter,
7979
llmGateway: llmGatewayRouter,
8080
logs: logsRouter,

packages/host-router/src/routers/github-releases.router.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)