Skip to content

Commit 1970f81

Browse files
authored
Better update instructions (#228)
1 parent 6ba80be commit 1970f81

File tree

3 files changed

+350
-3
lines changed

3 files changed

+350
-3
lines changed

CONTRIBUTING.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
# Release process:
22

3+
## Semi-automated release process (preferred)
4+
5+
1. Run `./publish_process.js` (with Node installed)
6+
- Choose to bump the patch, minor or major version as asked.
7+
- Fix the ChangeLog that is automatically populated and opened for you.
8+
- If there is a new release of Dafny, it will suggest to bump the Dafny version.
9+
Then it creates the branch, pushes it and give you a link for the PR.
10+
2. Create the PR, have it reviewed and merged (adding more commits is fine)
11+
3. Relaunch the script. It will detect the PR merge;
12+
- Confirm the publication of the new version.
13+
14+
## Manual release process stps
15+
316
1. Look for all the recent changes since the last version https://github.com/dafny-lang/ide-vscode/commits/master
417
and write a summary of each relevant commit in `CHANGELOG.md`
518
2. Update `package.json`
619
- Upgrade the version number of the extension in `package.json` (line 5) (let's assume it's A.B.C)
720
- [If Dafny changed] Search for `"dafny.preferredVersion":`, and add the most recent Dafny version number to the head of the list (let's assume it's X.Y.Z)
821
3. [If Dafny changed] Update `src/constants.ts`
922
- Change `LanguageServerConstants.LatestVersion = "X.Y.Z"` for the same version number that you put.
10-
4. Commit your changes in a branch named `dafny-X.Y.Z` (if Dafny changed) or `tag-A.B.C` (if only the VSCode extension changed)
11-
Your commit message could be `chore: Bump version to A.B.C`
23+
4. Commit your changes in a branch named `release-A.B.C
24+
Your commit message could be `chore: Release vA.B.C` optionally adding ` with support for Dafny X.Y.Z`
1225
5. Push this branch on the server, have it merged (after necessary approval)
1326
6. Pull the most recent master branch of the extension.
1427
7. Add the tag with the command `git tag vA.B.C` and push it with `git push origin vA.B.C`

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@
203203
"enum": [
204204
"latest",
205205
"3.7.3",
206-
"3.7.2",
207206
"3.6.0",
208207
"3.5.0",
209208
"3.4.2",

publish_process.js

+335
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
Perform the next step to publish the extension.
5+
i. Verifies that it's on the master branch. If not so, terminate. Otherwise, pulls the latest master version.
6+
ii. If the last message on master indicates Version X.Y.Z and it does not have a tag.
7+
it means that steps iii. to vii. were executed correctly.
8+
Ask whether it's ready to publish (ENTER or y)
9+
- if so, create and publish the tag
10+
- Otherwise, cancel.
11+
12+
iii. Fetches all tags, and verify that there is no version tag.
13+
iv. Asks to bump the patch version (ENTER or p), the minor version (m) or the major version (M)
14+
v. Populates the CHANGELOG.md with the most recent commit messages.
15+
vi. If a newest version of Dafny is found, ask whether to refer to it as the latest (ENTER or y), or no (n)
16+
vii. Follows the steps of CONTRIBUTING.
17+
Ask to revise the CHANGELOG.md as needed
18+
(n or anything else) : Abruptly stop.
19+
(ENTER or y) : Creates a branch named "version-X.Y.Z"
20+
Commits the files to this branch with message "Version X.Y.Z"
21+
Push the branch.
22+
Indicate that the PR should be merge without changing the commit message.
23+
Indicate that the script just needs to be relaunched once the PR is approved and merged.
24+
*/
25+
26+
const fs = require("fs");
27+
const { promisify, getSystemErrorMap } = require('util');
28+
const exec = require('child_process').exec;
29+
const execAsync = promisify(exec);
30+
const readline = require('readline');
31+
const fetch = (url, init) =>
32+
import('node-fetch').then(({ default: fetch }) => fetch(url, init));
33+
const rl = readline.createInterface({
34+
input: process.stdin,
35+
output: process.stdout
36+
});
37+
const question = function(input) {
38+
return new Promise((resolve, reject) => {
39+
rl.question(input, resolve);
40+
});
41+
}
42+
const changeLogFile = "CHANGELOG.md";
43+
const dafnyReleasesURL = 'https://api.github.com/repos/dafny-lang/dafny/releases';
44+
const constantsFile = "src/constants.ts";
45+
const packageFile = "package.json";
46+
const packageLockFile = "package-lock.json";
47+
const ABORTED = "ABORTED";
48+
const ACCEPT_HINT = "(ENTER)";
49+
50+
function ok(answer) {
51+
return answer.toLowerCase() == "y" || answer == "";
52+
}
53+
54+
async function getCurrentBranch() {
55+
return (await execAsync("git branch --show-current")).stdout.trim();
56+
}
57+
58+
// Ensures that the working directory is clean
59+
async function ensureWorkingDirectoryClean() {
60+
var unstagedChanges = (await execAsync("git diff")).stdout.trim() + (await execAsync("git diff --cached")).stdout.trim();
61+
if(unstagedChanges != "") {
62+
console.log("Please commit your changes before launching this script.");
63+
throw ABORTED;
64+
}
65+
}
66+
67+
async function ensureMaster() {
68+
await ensureWorkingDirectoryClean();
69+
var currentBranch = getCurrentBranch();
70+
if(currentBranch != "master") {
71+
console.log(`You need to be on the 'master' branch to release a new version.`);
72+
if(!ok(await question(`Switch from '${currentBranch}' to 'master'? ${ACCEPT_HINT}`))) {
73+
console.log("Publishing script aborted.");
74+
throw ABORTED;
75+
}
76+
console.log("switched to master branch");
77+
console.log((await execAsync("git checkout master")).stdout);
78+
currentBranch = getCurrentBranch();
79+
if(currentBranch != "master") {
80+
console.log("Failed to checkout master");
81+
throw ABORTED;
82+
}
83+
}
84+
await execAsync("git pull");
85+
console.log("Latest master checked out")
86+
}
87+
88+
async function isTagMissing() {
89+
var tagList = (await execAsync("git show-ref --tags")).stdout.trim();
90+
// recovers the last commit hash
91+
var lastCommitHash = (await execAsync("git log -1 --pretty=%H")).stdout.trim();
92+
// checks if the last commit hash is in the tag list:
93+
var tagListRegex = new RegExp(`^${lastCommitHash}\\s*refs/tags/(\\w*)`);
94+
var match = tagListRegex.exec(tagList);
95+
if(match == null) {
96+
return true;
97+
}
98+
console.log(`The current master already has the tag ${match[1]}. Nothing needs to be done.\nIf you want to push the tag again, run 'git push --tags'`);
99+
throw ABORTED;
100+
}
101+
102+
async function changeLogAndVersion() {
103+
let changeLog = await fs.promises.readFile(changeLogFile, "utf8");
104+
const currentDocumentVersionRegex = /^#\s*Release Notes\s*##\s*(\d+.\d+.\d+)/;
105+
const match = currentDocumentVersionRegex.exec(changeLog);
106+
if(match == null) {
107+
console.log(`Could not find ${currentDocumentVersionRegex} in ${changeLogFile}`);
108+
throw ABORTED;
109+
}
110+
const currentChangeLogVersion = match[1];
111+
const updateChangeLogWith = ((changeLog, oldVersion) => async function(newVersion, messages) {
112+
const newChangeLog = changeLog.replace(currentDocumentVersionRegex, match =>
113+
`# Release Notes\n\n## ${newVersion}\n${messages}\n\n## ${oldVersion}`);
114+
await fs.promises.writeFile(changeLogFile, newChangeLog);
115+
return true;
116+
})(changeLog, currentChangeLogVersion);
117+
return {updateChangeLogWith, currentChangeLogVersion};
118+
}
119+
120+
async function getMostRecentDafnyRelease() {
121+
let mostRecentDafnyRelease = null;
122+
const dafnyReleases = await (await fetch(dafnyReleasesURL)).json();
123+
for(var i = 0; i < dafnyReleases.length && mostRecentDafnyRelease == null; i++) {
124+
if(dafnyReleases[i].tag_name != "nightly") {
125+
mostRecentDafnyRelease = dafnyReleases[i].tag_name;
126+
break;
127+
}
128+
}
129+
if(mostRecentDafnyRelease == null) {
130+
console.log(`Could not fetch the latest Dafny release version from ${dafnyReleasesURL}`);
131+
throw ABORTED;
132+
}
133+
return mostRecentDafnyRelease;
134+
}
135+
136+
137+
async function readPackageJson() {
138+
const packageContent = await fs.promises.readFile(packageFile);
139+
const packageObj = JSON.parse(packageContent);
140+
return packageObj;
141+
}
142+
143+
async function writePackage(packageObj) {
144+
await fs.promises.writeFile(packageFile, JSON.stringify(packageObj, null, 2));
145+
}
146+
147+
// returns the new version number
148+
async function nextVersion(currentVersion) {
149+
const currentVersionRegex = /(\d+)\.(\d+)\.(\d+)/;
150+
const match = currentVersionRegex.exec(currentVersion);
151+
if(match == null) {
152+
console.log(`Could not parse version ${currentVersion}`);
153+
throw ABORTED;
154+
}
155+
var bumpedPatch = `${match[1]}.${match[2]}.${parseInt(match[3]) + 1}`;
156+
var bumpedMinor = `${match[1]}.${parseInt(match[2]) + 1}.0`;
157+
var bumpedMajor = `${parseInt(match[1]) + 1}.0.0`;
158+
console.log("Should the next version be:");
159+
console.log(`${currentVersion} => ${bumpedPatch}? ${ACCEPT_HINT}`);
160+
console.log(`${currentVersion} => ${bumpedMinor}? (m)`);
161+
var answer = await question(`${currentVersion} => ${bumpedMajor}? (M)\n`);
162+
if(ok(answer)) {
163+
return bumpedPatch;
164+
} else if(answer == "m") {
165+
return bumpedMinor;
166+
} else if(answer == "M") {
167+
return bumpedMajor;
168+
} else {
169+
console.log("Publishing script aborted.");
170+
throw ABORTED;
171+
}
172+
}
173+
174+
async function getLastPreparedTag() {
175+
const lastCommitMessage = (await execAsync("git log -1 --pretty=%B")).stdout.trim();
176+
const lastCommitMessageRegex = /(v\d+\.\d+\.\d+)/;
177+
const match = lastCommitMessageRegex.exec(lastCommitMessage);
178+
if(match == null) {
179+
return false;
180+
}
181+
return match[1];
182+
}
183+
184+
function getCommandLine() {
185+
switch (process.platform) {
186+
case 'darwin' : return 'open';
187+
case 'win32' : return 'start';
188+
case 'win64' : return 'start';
189+
default : return 'xdg-open';
190+
}
191+
}
192+
193+
async function getAllRecentCommitMessagesFormatted(currentChangeLogVersion) {
194+
// Query git for all the commit messages from currentChangeLogVersion (excluded) to the latest commit (included)
195+
var raw = (await execAsync(`git log --pretty=%B v${currentChangeLogVersion}..HEAD`)).stdout.trim();
196+
raw = "- " + raw.trim().replace(/\n\s*/g, "\n- ");
197+
raw = raw.replace(/\(#(\d+)\)/g, "(https://github.com/dafny-lang/ide-vscode/pull/$1)");
198+
raw = raw.trim();
199+
raw = raw.replace(/\n.*$/, ""); // Removes the last one, because it's the last release commit.
200+
return raw.trim();
201+
}
202+
203+
function close() {
204+
rl.close();
205+
return false;
206+
}
207+
208+
async function updatePackageJson(packageObj, newVersion, mostRecentDafnyRelease) {
209+
packageObj["version"] = newVersion;
210+
var versionList = packageObj["contributes"]["configuration"]["properties"]["dafny.preferredVersion"]["enum"];
211+
// versionList starts with "latest", and then the last version
212+
var previousDafnyVersion = versionList[1];
213+
var updatedDafny = false;
214+
if (previousDafnyVersion != mostRecentDafnyRelease) {
215+
if (ok(await question(`The Dafny version in the package.json file (${previousDafnyVersion}) is not the latest (${mostRecentDafnyRelease}). Do you want to update it? ${ACCEPT_HINT}`))) {
216+
var previousDafnyVersionListHead = versionList[1];
217+
// If the previous dafny version is just different from mostRecentDafnyRelease by the patch number, replace it, otherwise insert it using splice
218+
if (previousDafnyVersionListHead == mostRecentDafnyRelease.substring(0, mostRecentDafnyRelease.lastIndexOf("."))) {
219+
versionList[1] = mostRecentDafnyRelease;
220+
} else {
221+
versionList.splice(1, 0, mostRecentDafnyRelease);
222+
}
223+
224+
console.log("Updated Dafny version to " + mostRecentDafnyRelease);
225+
var constantsContent = await fs.promises.readAsync(constantsFile, "utf8");
226+
var constantsContentRegex = /const\s*LatestVersion\s*=\s*'\d+.\d+.\d+';/;
227+
constantsContent.replace(constantsContentRegex, `const LatestVersion = '${mostRecentDafnyRelease}';`);
228+
await fs.promises.writeAsync(constantsFile, constantsContent);
229+
updatedDafny = true;
230+
} else {
231+
console.log("Ignoring new Dafny version.");
232+
}
233+
}
234+
await writePackage(packageObj);
235+
return updatedDafny;
236+
}
237+
238+
async function UpdateChangeLog(currentChangeLogVersion, packageObj, updateChangeLogWith, newVersion) {
239+
var allRecentCommitMessages = await getAllRecentCommitMessagesFormatted(currentChangeLogVersion);
240+
if (packageObj["version"] == currentChangeLogVersion) {
241+
await updateChangeLogWith(newVersion, allRecentCommitMessages);
242+
console.log("I changed " + changeLogFile + " to reflect the new version.\nPlease make edits as needed and close the editing window.");
243+
await execAsync(getCommandLine() + ' ' + changeLogFile);
244+
if (!await question(`Ready to continue? ${ACCEPT_HINT}`)) {
245+
console.log("Aborting.");
246+
throw ABORTED;
247+
}
248+
currentChangeLogVersionCheck = (await changeLogAndVersion()).currentChangeLogVersion;
249+
if (currentChangeLogVersionCheck != newVersion) {
250+
console.log(`The last version was supposed to be ${newVersion}, but the changelog was updated to ${currentChangeLogVersionCheck}. Aborting publishing.`);
251+
throw ABORTED;
252+
}
253+
} else {
254+
console.log("ChangeLog.md already up-to-date");
255+
}
256+
return answer;
257+
}
258+
259+
async function HandleFinalPublishingProcess(currentChangeLogVersion, lastPreparedTag) {
260+
if ("v" + currentChangeLogVersion == lastPreparedTag) {
261+
// Tag the current commit
262+
console.log(`The changelog already mentions version ${currentChangeLogVersion}.\nYou now need to create the tag ${lastPreparedTag} and publish it to release this new version.`);
263+
// ask for confirmation, and publish the tag.
264+
if (ok(await question(`Create and publish the tag ${lastPreparedTag}? ${ACCEPT_HINT}`))) {
265+
console.log(`Creating tag ${lastPreparedTag}...`);
266+
await execAsync(`git tag ${lastPreparedTag}`);
267+
console.log(`Publishing tag ${lastPreparedTag}...`);
268+
await execAsync(`git push origin ${lastPreparedTag}`);
269+
console.log(`${lastPreparedTag} published. The CI will take care of releasing the new VSCode extension.`);
270+
} else {
271+
console.log("Just run the script again when you are ready to publish the version. Aborting.");
272+
throw ABORTED;
273+
}
274+
} else {
275+
console.log("Something went wrong. I found " + lastPreparedTag + " in the last commit message, and this tag is not published yet.");
276+
console.log("However, the changelog mentions a different version:" + currentChangeLogVersion);
277+
console.log("Please fix the current state");
278+
throw ABORTED;
279+
}
280+
}
281+
282+
async function Main() {
283+
try {
284+
// verify that we are on the master branch.
285+
await ensureMaster();
286+
await isTagMissing();
287+
let {updateChangeLogWith, currentChangeLogVersion} = await changeLogAndVersion();
288+
289+
const lastPreparedTag = await getLastPreparedTag();
290+
if(lastPreparedTag) {
291+
// Here we only need to publish the last prepared tag
292+
await HandleFinalPublishingProcess(currentChangeLogVersion, lastPreparedTag);
293+
return;
294+
}
295+
296+
let newVersion = await nextVersion(currentChangeLogVersion);
297+
let mostRecentDafnyRelease = await getMostRecentDafnyRelease().substring(1);
298+
let packageObj = await readPackageJson();
299+
300+
console.log(`Going to proceed to publish ${newVersion}`);
301+
// Get all the commit messages since the last published tag
302+
await UpdateChangeLog(currentChangeLogVersion, packageObj, updateChangeLogWith, newVersion);
303+
// All clear, we can modify constants.ts and package.json.
304+
305+
var updatedDafny = await updatePackageJson(packageObj, newVersion, mostRecentDafnyRelease);
306+
// Execute npm install to ensure the package lock is up to date
307+
console.log("Executing `npm install`...");
308+
await execAsync("npm install");
309+
310+
// Create the new branch and git add all the files modified above
311+
console.log("Creating new branch...");
312+
const newBranch = `release-${newVersion}`;
313+
await execAsync(`git checkout -b ${newBranch}`);
314+
await execAsync(`git add ${changeLogFile} ${packageFile} ${constantsFile} ${packageLockFile}`);
315+
if(ok(await question(`I made all the necessary edits. Push the changes to the remote repository? ${ACCEPT_HINT}`))) {
316+
await execAsync(`git commit -m "Release v${newVersion}${ updatedDafny ? ` (updated Dafny to ${mostRecentDafnyRelease})` : "" }"`);
317+
await execAsync(`git push origin ${newBranch}`);
318+
console.log("Now, create the pull request by clicking the link below:");
319+
console.log(`https://github.com/dafny-lang/ide-vscode/compare/${newBranch}?expand=1`);
320+
console.log("When this PR is approved and merged, launch this script again to finish publishing the release.");
321+
} else {
322+
console.log("Aborting publishing.");
323+
return close();
324+
}
325+
326+
} catch(e) {
327+
if(e != ABORTED) {
328+
throw e;
329+
}
330+
} finally {
331+
close();
332+
}
333+
}
334+
Main();
335+

0 commit comments

Comments
 (0)