Skip to content

Commit

Permalink
ci: fix missing imports, bad func usage, add fail check when release …
Browse files Browse the repository at this point in the history
…in progress (#1261)

<!-- For Coveo Employees only. Fill this section.

CDX-724

-->

## Proposed changes

- Fixes missing imports that caused
https://github.com/coveo/cli/actions/runs/4597808599/jobs/8121171408 ❌
- Fix-it-twice: Typescript checks on the JS files. (I elected a JS check
instead of using compilation or ts-node to KISS)
- Add further protection against merging while a release is in progress

## New protections

Currently (assuming it was working), if a PR has all its checks ✅, we as
admin could still merge it.
The only solution to prevent an admin to merge is to make a check of the
PR to fail
To do that, I used the logic we had for the 'rogue' package-lock
protection: a check that exit 1 or 0 depending on the presence of a file
or not.
To enforce it, I need to "Requires branches to be up to date before
merging", otherwise no further checks would be made.

So now to lock the branch:
 - We verify the main branch hasn't changed
- We write and commit-push a 'lockfile' into the repo: this is the
actual 'lock': from here, until this file get removed from master,
nothing can go in or out.
- We reset the local to the commit before (indeed we don't want to keep
this file or commit)
 - We continue the process as usual
- When we set where `master` should ref, we use 'force' because it is
not a fast-forward operation. (i.e. we want to remove 'gitlock'
  • Loading branch information
louis-bompart authored Apr 3, 2023
1 parent f4cfde4 commit d4309a4
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 17 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/git-lock-fail.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: 'master lock validator'

on:
pull_request:
branches: [master]
paths:
- '.git-lock'

jobs:
git-locked:
runs-on: ubuntu-20.04
steps:
- run: exit 1
13 changes: 13 additions & 0 deletions .github/workflows/git-lock-success.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: 'master lock validator'

on:
pull_request:
branches: [master]
paths-ignore:
- '.git-lock'

jobs:
git-locked:
runs-on: ubuntu-20.04
steps:
- run: exit 0
27 changes: 26 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 23 additions & 2 deletions utils/release/git-lock.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
#!/usr/bin/env node

import {gitPull, getSHA1fromRef} from '@coveo/semantic-monorepo-tools';
import dedent from 'ts-dedent';
import {
gitPull,
getSHA1fromRef,
gitCommit,
gitPush,
gitAdd,
} from '@coveo/semantic-monorepo-tools';
import {dedent} from 'ts-dedent';

import {
limitWriteAccessToBot,
removeWriteAccessRestrictions,
} from './lock-master.mjs';
import {writeFileSync} from 'node:fs';
import {spawnSync} from 'node:child_process';

const isPrerelease = process.env.IS_PRERELEASE === 'true';
const noLockRequired = Boolean(process.env.NO_LOCK);
const PATH = '.';

const ensureUpToDateBranch = async () => {
// Lock-out master
Expand All @@ -29,6 +38,18 @@ const ensureUpToDateBranch = async () => {
}
};

/**
* This will make .github\workflows\git-lock-fail.yml run and thus fail the associated check.
*/
const lockBranch = async () => {
writeFileSync('.git-lock', '');
await gitAdd('.git-lock');
await gitCommit('lock master', PATH);
await gitPush();
spawnSync('git', ['reset', '--hard', 'HEAD~1']);
};

if (!(isPrerelease || noLockRequired)) {
await ensureUpToDateBranch();
await lockBranch();
}
29 changes: 24 additions & 5 deletions utils/release/git-publish-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ import {
npmBumpVersion,
getSHA1fromRef,
gitSetupUser,
gitCreateBranch,
gitCheckoutBranch,
gitAdd,
gitWriteTree,
gitCommitTree,
gitUpdateRef,
gitPublishBranch,
} from '@coveo/semantic-monorepo-tools';
import {Octokit} from 'octokit';
import {createAppAuth} from '@octokit/auth-app';
// @ts-ignore no dts is ok.
import angularChangelogConvention from 'conventional-changelog-angular';
import {dedent} from 'ts-dedent';
import {readFileSync, writeFileSync} from 'fs';
Expand Down Expand Up @@ -66,9 +74,9 @@ const getCliChangelog = () => {
currentVersionTag.inc('prerelease');
const npmNewVersion = currentVersionTag.format();
// Write release version in the root package.json
await npmBumpVersion(npmNewVersion);
await npmBumpVersion(npmNewVersion, PATH);

const releaseNumber = currentVersionTag.prerelease;
const releaseNumber = currentVersionTag.prerelease[0];
const gitNewTag = `release-${releaseNumber}`;

// Find all changes since last release and generate the changelog.
Expand Down Expand Up @@ -136,7 +144,7 @@ const getCliChangelog = () => {
}
const releaseBody = getCliChangelog();
const cliLatestTag = cliReleaseInfoMatch[0];
const cliVersion = cliReleaseInfoMatch.groups.version;
const cliVersion = cliReleaseInfoMatch?.groups?.version;
await octokit.rest.repos.createRelease({
owner: REPO_OWNER,
repo: REPO_NAME,
Expand All @@ -146,6 +154,13 @@ const getCliChangelog = () => {
});
})();

/**
* "Craft" the signed release commit.
* @param {string|number} releaseNumber
* @param {string} commitMessage
* @param {Octokit} octokit
* @returns {Promise<string>}
*/
async function commitChanges(releaseNumber, commitMessage, octokit) {
// Get latest commit and name of the main branch.
const mainBranchName = await getCurrentBranchName();
Expand Down Expand Up @@ -189,6 +204,7 @@ async function commitChanges(releaseNumber, commitMessage, octokit) {
repo: REPO_NAME,
ref: `refs/heads/${mainBranchName}`,
sha: commit.data.sha,
force: true,
});

// Delete the temp branch
Expand All @@ -200,7 +216,10 @@ function updateRootReadme() {
const usageRegExp = /^<!-- usage -->(.|\n)*<!-- usagestop -->$/m;
const cliReadme = readFileSync('packages/cli/core/README.md', 'utf-8');
let rootReadme = readFileSync('README.md', 'utf-8');
const cliUsage = usageRegExp.exec(cliReadme);
rootReadme.replace(usageRegExp, cliUsage[0]);
const cliUsage = usageRegExp.exec(cliReadme)?.[0];
if (!cliUsage) {
return;
}
rootReadme.replace(usageRegExp, cliUsage);
writeFileSync('README.md', rootReadme);
}
19 changes: 18 additions & 1 deletion utils/release/lock-master.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,37 @@ export const limitWriteAccessToBot = () => changeBranchRestrictions(true);
export const removeWriteAccessRestrictions = () =>
changeBranchRestrictions(false);

/**
* Lock/Unlock the main branch to only allow the 🤖 to write on it.
* @param {boolean} onlyBot
*/
async function changeBranchRestrictions(onlyBot) {
const octokit = new Octokit({auth: process.env.GITHUB_CREDENTIALS});
if (onlyBot) {
// Disallow direct write access to the team
await octokit.rest.repos.setTeamAccessRestrictions({
...COVEO_CLI_MASTER,
teams: [],
});

// Requires branches to be up to date before merging
await octokit.rest.repos.updateStatusCheckProtection({
...COVEO_CLI_MASTER,
strict: true,
});
// Requires admins to pass the status checks
await octokit.rest.repos.setAdminBranchProtection(COVEO_CLI_MASTER);
} else {
// Allow direct write access to the team
await octokit.rest.repos.setTeamAccessRestrictions({
...COVEO_CLI_MASTER,
teams: ['dx'],
});
// Allow admins to bypass the status checks
await octokit.rest.repos.deleteAdminBranchProtection(COVEO_CLI_MASTER);
// Do not requires branches to be up to date before merging
await octokit.rest.repos.updateStatusCheckProtection({
...COVEO_CLI_MASTER,
strict: false,
});
}
}
57 changes: 49 additions & 8 deletions utils/release/npm-publish-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ import {
} from '@coveo/semantic-monorepo-tools';
import {spawnSync} from 'node:child_process';
import {appendFileSync, readFileSync, writeFileSync} from 'node:fs';
// @ts-ignore no dts is ok
import angularChangelogConvention from 'conventional-changelog-angular';
import {dirname, resolve, join} from 'node:path';
import {fileURLToPath} from 'node:url';
import retry from 'async-retry';
import {inc, compareBuild, gte} from 'semver';
import {inc, compareBuild, gte, SemVer} from 'semver';
import {json as fetchNpm} from 'npm-registry-fetch';

/**
* Check if the package json in the provided folder has changed since the last commit
* @param {string} directoryPath
* @returns {boolean}
*/
const hasPackageJsonChanged = (directoryPath) => {
const {stdout, stderr, status} = spawnSync(
'git',
Expand Down Expand Up @@ -61,9 +67,11 @@ const isPrerelease = process.env.IS_PRERELEASE === 'true';
}
const parsedCommits = parseCommits(commits, convention.parserOpts);
let currentGitVersion = getCurrentVersion(PATH);
let currentNpmVersion = privatePackage
? '0.0.0' // private package does not have a npm version, so we default to the 'lowest' possible
: await describeNpmTag(packageJson.name, 'latest');
let currentNpmVersion = new SemVer(
privatePackage
? '0.0.0' // private package does not have a npm version, so we default to the 'lowest' possible
: await describeNpmTag(packageJson.name, 'latest')
);
const isRedo = gte(currentNpmVersion, currentGitVersion);
const bumpInfo = isRedo
? {type: 'patch'}
Expand Down Expand Up @@ -117,6 +125,10 @@ const isPrerelease = process.env.IS_PRERELEASE === 'true';
);
})();

/**
* Update the version of the package in the other packages of the workspace
* @param {string} version
*/
async function updateWorkspaceDependent(version) {
const topology = JSON.parse(
readFileSync(join(rootFolder, 'topology.json'), {encoding: 'utf-8'})
Expand All @@ -134,7 +146,8 @@ async function updateWorkspaceDependent(version) {
)) {
if (
dependencies.find(
(dependency) => dependency.target === dependencyPackageName
(/** @type {{target:string}} **/ dependency) =>
dependency.target === dependencyPackageName
)
) {
dependentPackages.push(name);
Expand All @@ -158,6 +171,12 @@ async function updateWorkspaceDependent(version) {
}
}

/**
* Update all instancies of the `dependency` in the `packageJson` to the given `version`.
* @param {any} packageJson the packageJson object to update
* @param {string} dependency the dependency to look for and update
* @param {string} version the version to update to.
*/
function updateDependency(packageJson, dependency, version) {
for (const dependencyType of [
'dependencies',
Expand All @@ -170,6 +189,11 @@ function updateDependency(packageJson, dependency, version) {
}
}

/**
* Append `.cmd` to the input if the runtime OS is Windows.
* @param {string} cmd
* @returns
*/
export const appendCmdIfWindows = (cmd) =>
`${cmd}${process.platform === 'win32' ? '.cmd' : ''}`;

Expand All @@ -180,15 +204,32 @@ function isPrivatePackage() {
return packageJson.private;
}

/**
* Compute the next beta version of the package,
* based on what the next gold version would be,
* and the latest beta version published.
*
* @param {string} packageName
* @param {string} nextGoldVersion
* @returns {Promise<string>}
*/
async function getNextBetaVersion(packageName, nextGoldVersion) {
let nextBetaVersion = `${nextGoldVersion}-0`;

const registryMeta = await fetchNpm(packageName);

/**
* @type {string[]}
*/
// @ts-ignore force-cast: `fetchNpm` do not returns a `Record<string,unknown>, which is hard to work with.
const versions = Object.keys(registryMeta.versions);
const nextGoldMatcher = new RegExp(`${nextGoldVersion}-\\d+`);
const matchingPreReleasedVersions = versions
.filter((version) => nextGoldMatcher.test(version))
.sort(compareBuild);
if (matchingPreReleasedVersions.length === 0) {
return `${nextGoldVersion}-0`;
const lastPrerelease = matchingPreReleasedVersions.pop();
if (lastPrerelease) {
nextBetaVersion = inc(lastPrerelease, 'prerelease') ?? nextBetaVersion;
}
return inc(matchingPreReleasedVersions.pop(), 'prerelease');
return nextBetaVersion;
}
7 changes: 7 additions & 0 deletions utils/release/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@
"semver": "7.3.8",
"ts-dedent": "2.2.0"
},
"devDependencies": {
"@types/conventional-changelog-writer": "^4.0.2",
"typescript": "4.9.5"
},
"bin": {
"git-lock": "./git-lock.mjs",
"npm-publish": "./npm-publish-package.mjs",
"git-publish-all": "./git-publish-all.mjs"
},
"scripts": {
"test": "tsc"
}
}
13 changes: 13 additions & 0 deletions utils/release/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "nodenext",
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"noEmit": true
},
"exclude": ["node_modules"],
"include": ["*.mjs"]
}

0 comments on commit d4309a4

Please sign in to comment.