Skip to content
Draft
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
105 changes: 105 additions & 0 deletions .github/workflows/release-suite-release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: "[Release] suite release notes"

permissions:
contents: read # for actions/checkout

on:
workflow_dispatch:
release:
# Editing a release is included on purpose - release notes are regularly polished after publishing.
types: [published, edited]
schedule:
# Runs at 00:30 UTC every day, as a safety net for missed release events.
- cron: "30 0 * * *"
pull_request:
paths:
- .github/workflows/release-suite-release-notes.yml
- suite-common/release-notes/**

concurrency:
# Pull requests never deploy, so they are grouped per branch. Everything else shares one group to
# keep the uploads to S3 serialized.
group: ${{ github.workflow }}-${{ github.head_ref || 'deploy' }}
cancel-in-progress: false

jobs:
generate:
if: github.repository == 'trezor/trezor-suite'
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6

- name: Install SafeDep PMG
uses: safedep/pmg@26d5c0ad71fd0e5f48c3183c9c74d4b3eb7f57c8 # v0.19.1
continue-on-error: true
with:
version: v0.19.1

- name: Generate release notes manifests
run: |
yarn workspaces focus @suite-common/release-notes
yarn workspace @suite-common/release-notes generate-manifests

- name: Upload release notes manifests as an artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-notes-manifests
path: suite-common/release-notes/files
if-no-files-found: error

# Signing and uploading happen behind the environment approval gate. Keeping the private key out of
# the generate job is the whole point of the split - otherwise every run of this workflow could
# produce a validly signed manifest without anyone approving it.
publish:
needs: generate
if: github.event_name != 'pull_request'
environment: production-release-notes
runs-on: ubuntu-latest
permissions:
contents: read # for actions/checkout
id-token: write # for fetching the OIDC token
env:
AWS_REGION: "eu-central-1"
AWS_CLOUDFRONT_ID: E1ERY5K2OTKKI1
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6

# The manifests are never regenerated here - what gets published must be exactly what the
# approver reviewed. GitHub releases can be edited while the run waits for approval.
- name: Download release notes manifests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-notes-manifests
path: suite-common/release-notes/files

- name: Install SafeDep PMG
uses: safedep/pmg@26d5c0ad71fd0e5f48c3183c9c74d4b3eb7f57c8 # v0.19.1
continue-on-error: true
with:
version: v0.19.1

- name: Sign release notes manifests
env:
JWS_PRIVATE_KEY_ENV: ${{ secrets.JWS_PRIVATE_KEY_ENV }}
run: |
yarn workspaces focus @suite-common/release-notes
yarn workspace @suite-common/release-notes sign-manifests

- name: Configure aws credentials
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6
with:
role-to-assume: arn:aws:iam::538326561891:role/gh_actions_suite_production_release_notes
aws-region: ${{ env.AWS_REGION }}
role-duration-seconds: 900 # The default is 1h, 15min is enough for our use case

- name: Upload release notes manifests to S3
run: |
aws s3 cp suite-common/release-notes/files/desktop/releases.v1.jws \
s3://data.trezor.io/suite/release-notes/desktop/releases.v1.jws --cache-control no-cache
aws s3 cp suite-common/release-notes/files/mobile/releases.v1.jws \
s3://data.trezor.io/suite/release-notes/mobile/releases.v1.jws --cache-control no-cache
aws cloudfront create-invalidation --distribution-id ${AWS_CLOUDFRONT_ID} --paths '/suite/release-notes/*'
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ packages/suite-data/files/browser-detection
packages/suite-data/files/guide
packages/suite-data/files/translations/master.json

# @suite-common/release-notes (generated by generate-manifests)
suite-common/release-notes/files

# @trezor/connect-explorer
**/build-webextension/**

Expand Down
1 change: 1 addition & 0 deletions packages/env-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ export const {
getJWSPublicKey,
} = envUtils;

export { publicKey } from './jws';
export { resolveStaticPath, resolveConnectPath } from './resolveStaticPath';
2 changes: 2 additions & 0 deletions packages/suite/mocks/mockInitialAppState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { type FirmwareUpdateState } from '@suite-common/firmware';
import { messageSystemInitialState } from '@suite-common/message-system';
import { type MetadataState } from '@suite-common/metadata-types';
import { receiveInitialState } from '@suite-common/receive';
import { releaseNotesInitialState } from '@suite-common/release-notes';
import { quotaManagerInitialState } from '@suite-common/suite-sync-quota-manager';
import { type NetworkSymbol } from '@suite-common/wallet-config';

Expand All @@ -33,6 +34,7 @@ export const mockInitialAppState: AppState = {
torBootstrap: null,
},
suiteSettings: suiteSettingsInitialState,
releaseNotes: releaseNotesInitialState,
debug: debugInitialState,
flags: flagsInitialState,
locks: locksInitialState,
Expand Down
1 change: 1 addition & 0 deletions packages/suite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@suite-common/react-query": "workspace:*",
"@suite-common/receive": "workspace:*",
"@suite-common/redux-utils": "workspace:*",
"@suite-common/release-notes": "workspace:*",
"@suite-common/sentry": "workspace:*",
"@suite-common/staking": "workspace:*",
"@suite-common/suite-config": "workspace:*",
Expand Down
7 changes: 7 additions & 0 deletions packages/suite/src/actions/suite/storageActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,13 @@ export const saveDiscreetMode = () => async (_dispatch: Dispatch, getState: GetS
await db.addItem('discreetMode', getState().discreetMode, 'discreetMode', true);
};

export const saveReleaseNotes = () => async (_dispatch: Dispatch, getState: GetState) => {
if (!db.isAccessible()) return;
const { releases, fetchedAt, generatedAt } = getState().releaseNotes;

await db.addItem('releaseNotes', { releases, fetchedAt, generatedAt }, 'releaseNotes', true);
};

export const saveBackend =
(symbol: NetworkSymbol) => async (_dispatch: Dispatch, getState: GetState) => {
if (!db.isAccessible()) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { getReleaseUrl } from '@suite/github';
import { Translation } from '@suite/intl';
import { Badge, Card, Paragraph, Row, TextButton } from '@trezor/components';
import { isDesktop } from '@trezor/env-utils';

import { FormattedDate, MarkdownWithComponents } from 'src/components/suite';

import { ReleaseNotesUpdateButton } from './ReleaseNotesUpdateButton';

type ReleaseNoteCardProps = {
version: string;
publishedAt: string;
notes: string;
isCurrent: boolean;
isUpdatable: boolean;
};

const GithubFooter = ({ version }: { version: string }) => (
<TextButton
href={getReleaseUrl(version)}
target="_blank"
intent="neutral"
priority="secondary"
size="small"
isUnderlined
>
<Translation id="TR_CHANGELOG_ON_GITHUB" />
</TextButton>
);

export const ReleaseNoteCard = ({
version,
publishedAt,
notes,
isCurrent,
isUpdatable,
}: ReleaseNoteCardProps) => (
<Card
header={
<Row justifyContent="space-between" alignItems="center" gap={12}>
<Row gap={8} alignItems="center">
<Paragraph typographyStyle="body-sm" intent="neutral" priority="secondary">
<Translation id="TR_RELEASE_NOTES_VERSION" />
</Paragraph>
<Badge intent="neutral" size="small">
{version}
</Badge>
{isCurrent && (
<Badge intent="info" size="small">
<Translation id="TR_RELEASE_NOTES_YOUR_VERSION" />
</Badge>
)}
</Row>
<Row gap={12} alignItems="center">
{!!publishedAt && (
<Paragraph typographyStyle="body-xs" intent="neutral" priority="secondary">
<FormattedDate value={publishedAt} date />
</Paragraph>
)}
{isUpdatable && isDesktop() && <ReleaseNotesUpdateButton />}
</Row>
</Row>
}
footer={<GithubFooter version={version} />}
>
<MarkdownWithComponents>{notes}</MarkdownWithComponents>
</Card>
);
Original file line number Diff line number Diff line change
@@ -1,68 +1,17 @@
import { useCallback, useEffect, useState } from 'react';

import { getReleaseUrl } from '@suite/github';
import { Translation } from '@suite/intl';
import { Badge, Banner, Card, Paragraph, Row, TextButton } from '@trezor/components';

import { MarkdownWithComponents } from 'src/components/suite';

type State = { status: 'loading' } | { status: 'loaded'; changelog: string } | { status: 'error' };

type ReleaseHeaderProps = {
version: string;
};
import { Banner, Card, Column } from '@trezor/components';

const ReleaseHeader = ({ version }: ReleaseHeaderProps) => (
<Row gap={12} alignItems="center">
<Paragraph typographyStyle="body-sm" intent="neutral" priority="secondary">
<Translation id="TR_RELEASE_NOTES_VERSION" />
</Paragraph>
<Badge intent="neutral" size="small">
{version}
</Badge>
</Row>
);

const GithubFooter = ({ url }: { url: string }) => (
<TextButton
href={url}
target="_blank"
intent="neutral"
priority="secondary"
size="small"
isUnderlined
>
<Translation id="TR_CHANGELOG_ON_GITHUB" />
</TextButton>
);
import { ReleaseNoteCard } from './ReleaseNoteCard';
import { useReleaseNotes } from './useReleaseNotes';

export const ReleaseNotes = () => {
const [state, setState] = useState<State>({ status: 'loading' });
const suiteCurrentVersion = process.env.VERSION || '';

const loadLocalChangelog = useCallback(async () => {
try {
const response = await fetch((process.env.ASSET_PREFIX ?? '') + '/release-notes.md');
if (!response.ok) {
setState({ status: 'error' });

return;
}
setState({ status: 'loaded', changelog: await response.text() });
} catch {
setState({ status: 'error' });
}
}, []);

useEffect(() => {
loadLocalChangelog();
}, [loadLocalChangelog]);
const { releases, updatableRelease, isLoading, hasFailed } = useReleaseNotes();

if (state.status === 'loading') {
if (isLoading) {
return <Banner isLoading description={<Translation id="TR_RELEASE_NOTES_LOADING" />} />;
}

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

return (
<Card
header={<ReleaseHeader version={suiteCurrentVersion} />}
footer={<GithubFooter url={getReleaseUrl(suiteCurrentVersion)} />}
>
<MarkdownWithComponents>{state.changelog}</MarkdownWithComponents>
</Card>
<Column gap={16}>
{releases.map(release => (
<ReleaseNoteCard
key={release.version}
version={release.version}
publishedAt={release.publishedAt}
notes={release.notes}
isCurrent={release.isCurrent}
isUpdatable={release.version === updatableRelease?.version}
/>
))}
</Column>
);
};
Loading
Loading