Skip to content

Commit f37de56

Browse files
committed
feat: Release notes update
1 parent 5216163 commit f37de56

35 files changed

Lines changed: 1124 additions & 63 deletions
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: "[Release] suite release notes"
2+
3+
permissions:
4+
id-token: write # for fetching the OIDC token
5+
contents: read # for actions/checkout
6+
7+
on:
8+
workflow_dispatch:
9+
release:
10+
# Editing a release is included on purpose - release notes are regularly polished after publishing.
11+
types: [published, edited]
12+
schedule:
13+
# Runs at 00:30 UTC every day, as a safety net for missed release events.
14+
- cron: "30 0 * * *"
15+
# TODO: remove the push trigger before merging, it is only here to test the workflow on a branch.
16+
push:
17+
branches:
18+
- release-notes-updates2
19+
pull_request:
20+
paths:
21+
- .github/workflows/release-suite-release-notes.yml
22+
- suite-common/release-notes/**
23+
24+
concurrency:
25+
# Pull requests never deploy, so they are grouped per branch. Everything else shares one group to
26+
# keep the uploads to S3 serialized.
27+
group: ${{ github.workflow }}-${{ github.head_ref || 'deploy' }}
28+
cancel-in-progress: false
29+
30+
jobs:
31+
release-release-notes:
32+
if: github.repository == 'trezor/trezor-suite'
33+
environment: ${{ github.event_name != 'pull_request' && 'production-release-notes' || '' }}
34+
runs-on: ubuntu-latest
35+
env:
36+
AWS_REGION: "eu-central-1"
37+
AWS_CLOUDFRONT_ID: E1ERY5K2OTKKI1
38+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
42+
43+
- name: Install SafeDep PMG
44+
uses: safedep/pmg@26d5c0ad71fd0e5f48c3183c9c74d4b3eb7f57c8 # v0.19.1
45+
continue-on-error: true
46+
with:
47+
version: v0.19.1
48+
49+
- name: Generate release notes manifests
50+
run: |
51+
yarn workspaces focus @suite-common/release-notes
52+
yarn workspace @suite-common/release-notes generate-manifests
53+
54+
- name: Upload release notes manifests as an artifact
55+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
56+
with:
57+
name: release-notes-manifests
58+
path: suite-common/release-notes/files
59+
if-no-files-found: error
60+
61+
- name: Configure aws credentials
62+
if: github.event_name != 'pull_request'
63+
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6
64+
with:
65+
role-to-assume: arn:aws:iam::538326561891:role/gh_actions_suite_production_release_notes
66+
aws-region: ${{ env.AWS_REGION }}
67+
role-duration-seconds: 900 # The default is 1h, 15min is enough for our use case
68+
69+
- name: Upload release notes manifests to S3
70+
if: github.event_name != 'pull_request'
71+
run: |
72+
aws s3 cp suite-common/release-notes/files/desktop/releases.json \
73+
s3://data.trezor.io/suite/release-notes/desktop/releases.json --cache-control no-cache
74+
aws s3 cp suite-common/release-notes/files/mobile/releases.json \
75+
s3://data.trezor.io/suite/release-notes/mobile/releases.json --cache-control no-cache
76+
aws cloudfront create-invalidation --distribution-id ${AWS_CLOUDFRONT_ID} --paths '/suite/release-notes/*'

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ packages/suite-data/files/browser-detection
137137
packages/suite-data/files/guide
138138
packages/suite-data/files/translations/master.json
139139

140+
# @suite-common/release-notes (generated by generate-manifests)
141+
suite-common/release-notes/files
142+
140143
# @trezor/connect-explorer
141144
**/build-webextension/**
142145

packages/suite/mocks/mockInitialAppState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { type FirmwareUpdateState } from '@suite-common/firmware';
1313
import { messageSystemInitialState } from '@suite-common/message-system';
1414
import { type MetadataState } from '@suite-common/metadata-types';
1515
import { receiveInitialState } from '@suite-common/receive';
16+
import { releaseNotesInitialState } from '@suite-common/release-notes';
1617
import { quotaManagerInitialState } from '@suite-common/suite-sync-quota-manager';
1718
import { type NetworkSymbol } from '@suite-common/wallet-config';
1819

@@ -33,6 +34,7 @@ export const mockInitialAppState: AppState = {
3334
torBootstrap: null,
3435
},
3536
suiteSettings: suiteSettingsInitialState,
37+
releaseNotes: releaseNotesInitialState,
3638
debug: debugInitialState,
3739
flags: flagsInitialState,
3840
locks: locksInitialState,

packages/suite/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"@suite-common/react-query": "workspace:*",
6666
"@suite-common/receive": "workspace:*",
6767
"@suite-common/redux-utils": "workspace:*",
68+
"@suite-common/release-notes": "workspace:*",
6869
"@suite-common/sentry": "workspace:*",
6970
"@suite-common/staking": "workspace:*",
7071
"@suite-common/suite-config": "workspace:*",

packages/suite/src/actions/suite/storageActions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,13 @@ export const saveDiscreetMode = () => async (_dispatch: Dispatch, getState: GetS
432432
await db.addItem('discreetMode', getState().discreetMode, 'discreetMode', true);
433433
};
434434

435+
export const saveReleaseNotes = () => async (_dispatch: Dispatch, getState: GetState) => {
436+
if (!db.isAccessible()) return;
437+
const { releases, fetchedAt } = getState().releaseNotes;
438+
439+
await db.addItem('releaseNotes', { releases, fetchedAt }, 'releaseNotes', true);
440+
};
441+
435442
export const saveBackend =
436443
(symbol: NetworkSymbol) => async (_dispatch: Dispatch, getState: GetState) => {
437444
if (!db.isAccessible()) return;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { getReleaseUrl } from '@suite/github';
2+
import { Translation } from '@suite/intl';
3+
import { Badge, Card, Paragraph, Row, TextButton } from '@trezor/components';
4+
import { isDesktop } from '@trezor/env-utils';
5+
6+
import { FormattedDate, MarkdownWithComponents } from 'src/components/suite';
7+
8+
import { ReleaseNotesUpdateButton } from './ReleaseNotesUpdateButton';
9+
10+
type ReleaseNoteCardProps = {
11+
version: string;
12+
publishedAt: string;
13+
notes: string;
14+
isCurrent: boolean;
15+
isUpdatable: boolean;
16+
};
17+
18+
const GithubFooter = ({ version }: { version: string }) => (
19+
<TextButton
20+
href={getReleaseUrl(version)}
21+
target="_blank"
22+
intent="neutral"
23+
priority="secondary"
24+
size="small"
25+
isUnderlined
26+
>
27+
<Translation id="TR_CHANGELOG_ON_GITHUB" />
28+
</TextButton>
29+
);
30+
31+
export const ReleaseNoteCard = ({
32+
version,
33+
publishedAt,
34+
notes,
35+
isCurrent,
36+
isUpdatable,
37+
}: ReleaseNoteCardProps) => (
38+
<Card
39+
header={
40+
<Row justifyContent="space-between" alignItems="center" gap={12}>
41+
<Row gap={8} alignItems="center">
42+
<Paragraph typographyStyle="body-sm" intent="neutral" priority="secondary">
43+
<Translation id="TR_RELEASE_NOTES_VERSION" />
44+
</Paragraph>
45+
<Badge intent="neutral" size="small">
46+
{version}
47+
</Badge>
48+
{isCurrent && (
49+
<Badge intent="info" size="small">
50+
<Translation id="TR_RELEASE_NOTES_YOUR_VERSION" />
51+
</Badge>
52+
)}
53+
</Row>
54+
<Row gap={12} alignItems="center">
55+
{!!publishedAt && (
56+
<Paragraph typographyStyle="body-xs" intent="neutral" priority="secondary">
57+
<FormattedDate value={publishedAt} date />
58+
</Paragraph>
59+
)}
60+
{isUpdatable && isDesktop() && <ReleaseNotesUpdateButton />}
61+
</Row>
62+
</Row>
63+
}
64+
footer={<GithubFooter version={version} />}
65+
>
66+
<MarkdownWithComponents>{notes}</MarkdownWithComponents>
67+
</Card>
68+
);
Lines changed: 18 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,17 @@
1-
import { useCallback, useEffect, useState } from 'react';
2-
3-
import { getReleaseUrl } from '@suite/github';
41
import { Translation } from '@suite/intl';
5-
import { Badge, Banner, Card, Paragraph, Row, TextButton } from '@trezor/components';
6-
7-
import { MarkdownWithComponents } from 'src/components/suite';
8-
9-
type State = { status: 'loading' } | { status: 'loaded'; changelog: string } | { status: 'error' };
10-
11-
type ReleaseHeaderProps = {
12-
version: string;
13-
};
2+
import { Banner, Card, Column } from '@trezor/components';
143

15-
const ReleaseHeader = ({ version }: ReleaseHeaderProps) => (
16-
<Row gap={12} alignItems="center">
17-
<Paragraph typographyStyle="body-sm" intent="neutral" priority="secondary">
18-
<Translation id="TR_RELEASE_NOTES_VERSION" />
19-
</Paragraph>
20-
<Badge intent="neutral" size="small">
21-
{version}
22-
</Badge>
23-
</Row>
24-
);
25-
26-
const GithubFooter = ({ url }: { url: string }) => (
27-
<TextButton
28-
href={url}
29-
target="_blank"
30-
intent="neutral"
31-
priority="secondary"
32-
size="small"
33-
isUnderlined
34-
>
35-
<Translation id="TR_CHANGELOG_ON_GITHUB" />
36-
</TextButton>
37-
);
4+
import { ReleaseNoteCard } from './ReleaseNoteCard';
5+
import { useReleaseNotes } from './useReleaseNotes';
386

397
export const ReleaseNotes = () => {
40-
const [state, setState] = useState<State>({ status: 'loading' });
41-
const suiteCurrentVersion = process.env.VERSION || '';
42-
43-
const loadLocalChangelog = useCallback(async () => {
44-
try {
45-
const response = await fetch((process.env.ASSET_PREFIX ?? '') + '/release-notes.md');
46-
if (!response.ok) {
47-
setState({ status: 'error' });
48-
49-
return;
50-
}
51-
setState({ status: 'loaded', changelog: await response.text() });
52-
} catch {
53-
setState({ status: 'error' });
54-
}
55-
}, []);
56-
57-
useEffect(() => {
58-
loadLocalChangelog();
59-
}, [loadLocalChangelog]);
8+
const { releases, updatableRelease, isLoading, hasFailed } = useReleaseNotes();
609

61-
if (state.status === 'loading') {
10+
if (isLoading) {
6211
return <Banner isLoading description={<Translation id="TR_RELEASE_NOTES_LOADING" />} />;
6312
}
6413

65-
if (state.status === 'error') {
14+
if (hasFailed || releases.length === 0) {
6615
return (
6716
<Card>
6817
<Translation id="TR_COULD_NOT_RETRIEVE_CHANGELOG" />
@@ -71,11 +20,17 @@ export const ReleaseNotes = () => {
7120
}
7221

7322
return (
74-
<Card
75-
header={<ReleaseHeader version={suiteCurrentVersion} />}
76-
footer={<GithubFooter url={getReleaseUrl(suiteCurrentVersion)} />}
77-
>
78-
<MarkdownWithComponents>{state.changelog}</MarkdownWithComponents>
79-
</Card>
23+
<Column gap={16}>
24+
{releases.map(release => (
25+
<ReleaseNoteCard
26+
key={release.version}
27+
version={release.version}
28+
publishedAt={release.publishedAt}
29+
notes={release.notes}
30+
isCurrent={release.isCurrent}
31+
isUpdatable={release.version === updatableRelease?.version}
32+
/>
33+
))}
34+
</Column>
8035
);
8136
};
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {
2+
UpdateState,
3+
downloadThunk,
4+
installUpdateThunk,
5+
selectDesktopUpdate,
6+
} from '@suite/desktop-update';
7+
import { Translation } from '@suite/intl';
8+
import { Button } from '@trezor/components';
9+
import { desktopApi } from '@trezor/suite-desktop-api';
10+
11+
import { useDispatch, useSelector } from 'src/hooks/suite';
12+
13+
/**
14+
* Turns the newest release in the list into a call to action. Rendered on desktop only — the web app
15+
* always runs the latest version, so there is nothing for the user to update.
16+
*/
17+
export const ReleaseNotesUpdateButton = () => {
18+
const desktopUpdate = useSelector(selectDesktopUpdate);
19+
20+
const dispatch = useDispatch();
21+
22+
if (!desktopUpdate.enabled) return null;
23+
24+
const downloadUpdate = () => {
25+
dispatch(downloadThunk());
26+
desktopApi.downloadUpdate();
27+
};
28+
29+
const installUpdate = () => dispatch(installUpdateThunk({ installNow: true }));
30+
31+
const checkForUpdates = () => desktopApi.checkForUpdates({ isManual: true });
32+
33+
const commonProps = { size: 'small', intent: 'brand' } as const;
34+
35+
if (desktopUpdate.state === UpdateState.Available) {
36+
return (
37+
<Button {...commonProps} onClick={downloadUpdate}>
38+
<Translation id="TR_UPDATE_MODAL_START_DOWNLOAD" />
39+
</Button>
40+
);
41+
}
42+
43+
if (desktopUpdate.state === UpdateState.Downloading) {
44+
return (
45+
<Button {...commonProps} isDisabled>
46+
<Translation id="SETTINGS_UPDATE_DOWNLOADING" />
47+
</Button>
48+
);
49+
}
50+
51+
if (desktopUpdate.state === UpdateState.Ready) {
52+
return (
53+
<Button {...commonProps} onClick={installUpdate}>
54+
<Translation id="SETTINGS_UPDATE_READY" />
55+
</Button>
56+
);
57+
}
58+
59+
if (desktopUpdate.state === UpdateState.Checking) {
60+
return (
61+
<Button {...commonProps} isDisabled>
62+
<Translation id="SETTINGS_UPDATE_CHECKING" />
63+
</Button>
64+
);
65+
}
66+
67+
// The manifest knows about a newer release the updater has not picked up yet.
68+
return (
69+
<Button {...commonProps} onClick={checkForUpdates}>
70+
<Translation id="SETTINGS_UPDATE_CHECK" />
71+
</Button>
72+
);
73+
};

0 commit comments

Comments
 (0)