Skip to content

Commit 1349cb2

Browse files
committed
Add in product changelog
1 parent b9743c9 commit 1349cb2

File tree

4 files changed

+334
-0
lines changed

4 files changed

+334
-0
lines changed

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"onCommand:gitpod.syncProvider.remove",
3737
"onCommand:gitpod.exportLogs",
3838
"onCommand:gitpod.api.autoTunnel",
39+
"onCommand:gitpod.showReleaseNotes",
3940
"onAuthenticationRequest:gitpod",
4041
"onUri"
4142
],
@@ -92,6 +93,11 @@
9293
"command": "gitpod.exportLogs",
9394
"category": "Gitpod",
9495
"title": "Export all logs"
96+
},
97+
{
98+
"command": "gitpod.showReleaseNotes",
99+
"category": "Gitpod",
100+
"title": "Show Release Notes"
95101
}
96102
]
97103
},
@@ -109,6 +115,7 @@
109115
"@types/analytics-node": "^3.1.9",
110116
"@types/crypto-js": "4.1.1",
111117
"@types/google-protobuf": "^3.7.4",
118+
"@types/js-yaml": "^4.0.5",
112119
"@types/node": "16.x",
113120
"@types/node-fetch": "^2.5.12",
114121
"@types/semver": "^7.3.10",
@@ -135,6 +142,7 @@
135142
"@gitpod/local-app-api-grpcweb": "main",
136143
"@improbable-eng/grpc-web-node-http-transport": "^0.14.0",
137144
"analytics-node": "^6.0.0",
145+
"js-yaml": "^4.1.0",
138146
"node-fetch": "2.6.7",
139147
"pkce-challenge": "^3.0.0",
140148
"semver": "^7.3.7",

src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { enableSettingsSync, updateSyncContext } from './settingsSync';
1212
import { GitpodServer } from './gitpodServer';
1313
import TelemetryReporter from './telemetryReporter';
1414
import { exportLogs } from './exportLogs';
15+
import { registerReleaseNotesView } from './releaseNotes';
1516

1617
const EXTENSION_ID = 'gitpod.gitpod-desktop';
1718
const FIRST_INSTALL_KEY = 'gitpod-desktop.firstInstall';
@@ -89,6 +90,8 @@ export async function activate(context: vscode.ExtensionContext) {
8990
await context.globalState.update(FIRST_INSTALL_KEY, true);
9091
telemetry.sendTelemetryEvent('gitpod_desktop_installation', { kind: 'install' });
9192
}
93+
94+
registerReleaseNotesView(context);
9295
}
9396

9497
export async function deactivate() {

src/releaseNotes.ts

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import fetch from 'node-fetch';
7+
import * as vscode from 'vscode';
8+
import { load } from 'js-yaml';
9+
10+
const LAST_READ_RELEASE_NOTES_ID = 'gitpod.lastReadReleaseNotesId';
11+
12+
export function registerReleaseNotesView(context: vscode.ExtensionContext) {
13+
14+
async function shouldShowReleaseNotes(lastReadId: string | undefined) {
15+
const releaseId = await getLastPublish();
16+
console.log(`gitpod release notes lastReadId: ${lastReadId}, latestReleaseId: ${releaseId}`);
17+
return releaseId !== lastReadId;
18+
}
19+
20+
context.subscriptions.push(
21+
vscode.commands.registerCommand('gitpod.showReleaseNotes', () => {
22+
ReleaseNotesPanel.createOrShow(context);
23+
})
24+
);
25+
26+
// sync between machines
27+
context.globalState.setKeysForSync([LAST_READ_RELEASE_NOTES_ID]);
28+
29+
const lastReadId = context.globalState.get<string>(LAST_READ_RELEASE_NOTES_ID);
30+
shouldShowReleaseNotes(lastReadId).then(shouldShow => {
31+
if (shouldShow) {
32+
ReleaseNotesPanel.createOrShow(context);
33+
}
34+
});
35+
}
36+
37+
async function getLastPublish() {
38+
const resp = await fetch(`${websiteHost}/changelog/latest`);
39+
if (!resp.ok) {
40+
throw new Error(`Getting latest releaseId failed: ${resp.statusText}`);
41+
}
42+
const { releaseId } = JSON.parse(await resp.text());
43+
return releaseId as string;
44+
}
45+
46+
const websiteHost = 'https://www.gitpod.io';
47+
48+
class ReleaseNotesPanel {
49+
public static currentPanel: ReleaseNotesPanel | undefined;
50+
public static readonly viewType = 'gitpodReleaseNotes';
51+
private readonly panel: vscode.WebviewPanel;
52+
private lastReadId: string | undefined;
53+
private _disposables: vscode.Disposable[] = [];
54+
55+
private async loadChangelog(releaseId: string) {
56+
const resp = await fetch(`${websiteHost}/changelog/raw-markdown?releaseId=${releaseId}`);
57+
if (!resp.ok) {
58+
throw new Error(`Getting raw markdown content failed: ${resp.statusText}`);
59+
}
60+
const md = await resp.text();
61+
62+
const parseInfo = (md: string) => {
63+
if (!md.startsWith('---')) {
64+
return;
65+
}
66+
const lines = md.split('\n');
67+
const end = lines.indexOf('---', 1);
68+
const content = lines.slice(1, end).join('\n');
69+
return load(content) as { title: string; date: string; image: string; alt: string; excerpt: string };
70+
};
71+
const info = parseInfo(md);
72+
73+
const content = md
74+
.replace(/---.*?---/gms, '')
75+
.replace(/<script>.*?<\/script>/gms, '')
76+
.replace(/<Badge.*?text="(.*?)".*?\/>/gim, '`$1`')
77+
.replace(/<Contributors usernames="(.*?)" \/>/gim, (_, p1) => {
78+
const users = p1
79+
.split(',')
80+
.map((e: string) => `[${e}](https://github.com/${e})`);
81+
return `Contributors: ${users.join(', ')}`;
82+
})
83+
.replace(/<p>(.*?)<\/p>/gm, '$1')
84+
.replace(/^[\n]+/m, '');
85+
if (!info) {
86+
return content;
87+
}
88+
89+
return [
90+
`# ${info.title}`,
91+
`> Published at ${releaseId}, see also https://gitpod.io/changelog`,
92+
`![${info.alt ?? 'image'}](https://www.gitpod.io/images/changelog/${info.image})`,
93+
content,
94+
].join('\n\n');
95+
}
96+
97+
public async updateHtml(releaseId?: string) {
98+
if (!releaseId) {
99+
releaseId = await getLastPublish();
100+
}
101+
const mdContent = await this.loadChangelog(releaseId);
102+
const html = await vscode.commands.executeCommand('markdown.api.render', mdContent) as string;
103+
this.panel.webview.html = `<!DOCTYPE html>
104+
<html lang="en">
105+
<head>
106+
<meta charset="UTF-8">
107+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
108+
<title>Gitpod Release Notes</title>
109+
<style>
110+
${DEFAULT_MARKDOWN_STYLES}
111+
</style>
112+
</head>
113+
<body>
114+
${html}
115+
</body>
116+
</html>`;
117+
if (!this.lastReadId || releaseId > this.lastReadId) {
118+
await this.context.globalState.update(LAST_READ_RELEASE_NOTES_ID, releaseId);
119+
this.lastReadId = releaseId;
120+
}
121+
}
122+
123+
public static createOrShow(context: vscode.ExtensionContext) {
124+
const column = vscode.window.activeTextEditor
125+
? vscode.window.activeTextEditor.viewColumn
126+
: undefined;
127+
128+
if (ReleaseNotesPanel.currentPanel) {
129+
ReleaseNotesPanel.currentPanel.panel.reveal(column);
130+
return;
131+
}
132+
133+
const panel = vscode.window.createWebviewPanel(
134+
ReleaseNotesPanel.viewType,
135+
'Gitpod Release Notes',
136+
column || vscode.ViewColumn.One,
137+
{ enableScripts: true },
138+
);
139+
140+
ReleaseNotesPanel.currentPanel = new ReleaseNotesPanel(context, panel);
141+
}
142+
143+
public static revive(context: vscode.ExtensionContext, panel: vscode.WebviewPanel) {
144+
ReleaseNotesPanel.currentPanel = new ReleaseNotesPanel(context, panel);
145+
}
146+
147+
private constructor(
148+
private readonly context: vscode.ExtensionContext,
149+
panel: vscode.WebviewPanel
150+
) {
151+
this.lastReadId = this.context.globalState.get<string>(LAST_READ_RELEASE_NOTES_ID);
152+
this.panel = panel;
153+
154+
this.updateHtml();
155+
156+
this.panel.onDidDispose(() => this.dispose(), null, this._disposables);
157+
this.panel.onDidChangeViewState(
158+
() => {
159+
if (this.panel.visible) {
160+
this.updateHtml();
161+
}
162+
},
163+
null,
164+
this._disposables
165+
);
166+
}
167+
168+
public dispose() {
169+
ReleaseNotesPanel.currentPanel = undefined;
170+
this.panel.dispose();
171+
while (this._disposables.length) {
172+
const x = this._disposables.pop();
173+
if (x) {
174+
x.dispose();
175+
}
176+
}
177+
}
178+
}
179+
180+
// Align with https://github.com/gitpod-io/openvscode-server/blob/494f7eba3615344ee634e6bec0b20a1903e5881d/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts#L14
181+
export const DEFAULT_MARKDOWN_STYLES = `
182+
body {
183+
padding: 10px 20px;
184+
line-height: 22px;
185+
max-width: 882px;
186+
margin: 0 auto;
187+
}
188+
189+
body *:last-child {
190+
margin-bottom: 0;
191+
}
192+
193+
img {
194+
max-width: 100%;
195+
max-height: 100%;
196+
}
197+
198+
a {
199+
text-decoration: none;
200+
}
201+
202+
a:hover {
203+
text-decoration: underline;
204+
}
205+
206+
a:focus,
207+
input:focus,
208+
select:focus,
209+
textarea:focus {
210+
outline: 1px solid -webkit-focus-ring-color;
211+
outline-offset: -1px;
212+
}
213+
214+
hr {
215+
border: 0;
216+
height: 2px;
217+
border-bottom: 2px solid;
218+
}
219+
220+
h1 {
221+
padding-bottom: 0.3em;
222+
line-height: 1.2;
223+
border-bottom-width: 1px;
224+
border-bottom-style: solid;
225+
}
226+
227+
h1, h2, h3 {
228+
font-weight: normal;
229+
}
230+
231+
table {
232+
border-collapse: collapse;
233+
}
234+
235+
table > thead > tr > th {
236+
text-align: left;
237+
border-bottom: 1px solid;
238+
}
239+
240+
table > thead > tr > th,
241+
table > thead > tr > td,
242+
table > tbody > tr > th,
243+
table > tbody > tr > td {
244+
padding: 5px 10px;
245+
}
246+
247+
table > tbody > tr + tr > td {
248+
border-top-width: 1px;
249+
border-top-style: solid;
250+
}
251+
252+
blockquote {
253+
margin: 0 7px 0 5px;
254+
padding: 0 16px 0 10px;
255+
border-left-width: 5px;
256+
border-left-style: solid;
257+
}
258+
259+
code {
260+
font-family: "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;
261+
}
262+
263+
pre code {
264+
font-family: var(--vscode-editor-font-family);
265+
font-weight: var(--vscode-editor-font-weight);
266+
font-size: var(--vscode-editor-font-size);
267+
line-height: 1.5;
268+
}
269+
270+
code > div {
271+
padding: 16px;
272+
border-radius: 3px;
273+
overflow: auto;
274+
}
275+
276+
.monaco-tokenized-source {
277+
white-space: pre;
278+
}
279+
280+
/** Theming */
281+
282+
.vscode-light code > div {
283+
background-color: rgba(220, 220, 220, 0.4);
284+
}
285+
286+
.vscode-dark code > div {
287+
background-color: rgba(10, 10, 10, 0.4);
288+
}
289+
290+
.vscode-high-contrast code > div {
291+
background-color: var(--vscode-textCodeBlock-background);
292+
}
293+
294+
.vscode-high-contrast h1 {
295+
border-color: rgb(0, 0, 0);
296+
}
297+
298+
.vscode-light table > thead > tr > th {
299+
border-color: rgba(0, 0, 0, 0.69);
300+
}
301+
302+
.vscode-dark table > thead > tr > th {
303+
border-color: rgba(255, 255, 255, 0.69);
304+
}
305+
306+
.vscode-light h1,
307+
.vscode-light hr,
308+
.vscode-light table > tbody > tr + tr > td {
309+
border-color: rgba(0, 0, 0, 0.18);
310+
}
311+
312+
.vscode-dark h1,
313+
.vscode-dark hr,
314+
.vscode-dark table > tbody > tr + tr > td {
315+
border-color: rgba(255, 255, 255, 0.18);
316+
}
317+
318+
`;

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@
177177
resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.6.tgz#674a69493ef2c849b95eafe69167ea59079eb504"
178178
integrity sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==
179179

180+
"@types/js-yaml@^4.0.5":
181+
version "4.0.5"
182+
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
183+
integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==
184+
180185
"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
181186
version "7.0.11"
182187
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"

0 commit comments

Comments
 (0)