This commit is contained in:
dexorder
2024-10-17 02:42:28 -04:00
commit 25def69c66
878 changed files with 112489 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env node
// Adjusts the format of the changelog that changesets generates.
// This is run automatically when npm version is run.
const fs = require('fs');
const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
// Groups:
// - 1: Pull Request Number and URL
// - 2: Changeset entry
const RELEASE_LINE_REGEX = /^- (\[#.*?\]\(.*?\))?.*?! - (.*)$/gm;
// Captures vX.Y.Z or vX.Y.Z-rc.W
const VERSION_TITLE_REGEX = /^## (\d+\.\d+\.\d+(-rc\.\d+)?)$/gm;
const isPrerelease = process.env.PRERELEASE === 'true';
const formatted = changelog
// Remove titles
.replace(/^### Major Changes\n\n/gm, '')
.replace(/^### Minor Changes\n\n/gm, '')
.replace(/^### Patch Changes\n\n/gm, '')
// Remove extra whitespace between items
.replace(/^(- \[.*\n)\n(?=-)/gm, '$1')
// Format each release line
.replace(RELEASE_LINE_REGEX, (_, pr, entry) => (pr ? `- ${entry} (${pr})` : `- ${entry}`))
// Add date to new version
.replace(VERSION_TITLE_REGEX, `\n## $1 (${new Date().toISOString().split('T')[0]})`)
// Conditionally allow vX.Y.Z.rc-.W sections only in prerelease
.replace(/^## \d\.\d\.\d-rc\S+[^]+?(?=^#)/gm, section => (isPrerelease ? section : ''));
fs.writeFileSync('CHANGELOG.md', formatted);

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env node
// Synchronizes the version in contracts/package.json with the one in package.json.
// This is run automatically when npm version is run.
const fs = require('fs');
setVersion('package.json', 'contracts/package.json');
function setVersion(from, to) {
const fromJson = JSON.parse(fs.readFileSync(from));
const toJson = JSON.parse(fs.readFileSync(to));
toJson.version = fromJson.version;
fs.writeFileSync(to, JSON.stringify(toJson, null, 2) + '\n');
}

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env node
const fs = require('fs');
const proc = require('child_process');
const semver = require('semver');
const run = (cmd, ...args) => proc.execFileSync(cmd, args, { encoding: 'utf8' }).trim();
const gitStatus = run('git', 'status', '--porcelain', '-uno', 'contracts/**/*.sol');
if (gitStatus.length > 0) {
console.error('Contracts directory is not clean');
process.exit(1);
}
const { version } = require('../../package.json');
// Get latest tag according to semver.
const [tag] = run('git', 'tag')
.split(/\r?\n/)
.filter(semver.coerce) // check version can be processed
.filter(v => semver.satisfies(v, `< ${version}`)) // ignores prereleases unless currently a prerelease
.sort(semver.rcompare);
// Ordering tag → HEAD is important here.
const files = run('git', 'diff', tag, 'HEAD', '--name-only', 'contracts/**/*.sol')
.split(/\r?\n/)
.filter(file => file && !file.match(/mock/i) && fs.existsSync(file));
for (const file of files) {
const current = fs.readFileSync(file, 'utf8');
const updated = current.replace(
/(\/\/ SPDX-License-Identifier:.*)$(\n\/\/ OpenZeppelin Contracts .*$)?/m,
`$1\n// OpenZeppelin Contracts (last updated v${version}) (${file.replace('contracts/', '')})`,
);
fs.writeFileSync(file, updated);
}

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
changeset version
scripts/release/format-changelog.js
scripts/release/synchronize-versions.js
scripts/release/update-comment.js
oz-docs update-version

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
npx changeset pre exit rc
git add .
git commit -m "Exit release candidate"
git push origin

View File

@@ -0,0 +1,48 @@
const { readFileSync } = require('fs');
const { join } = require('path');
const { version } = require(join(__dirname, '../../../package.json'));
module.exports = async ({ github, context }) => {
const changelog = readFileSync('CHANGELOG.md', 'utf8');
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${version}`,
target_commitish: github.ref_name,
body: extractSection(changelog, version),
prerelease: process.env.PRERELEASE === 'true',
});
};
// From https://github.com/frangio/extract-changelog/blob/master/src/utils/word-regexp.ts
function makeWordRegExp(word) {
const start = word.length > 0 && /\b/.test(word[0]) ? '\\b' : '';
const end = word.length > 0 && /\b/.test(word[word.length - 1]) ? '\\b' : '';
return new RegExp(start + [...word].map(c => (/[a-z0-9]/i.test(c) ? c : '\\' + c)).join('') + end);
}
// From https://github.com/frangio/extract-changelog/blob/master/src/core.ts
function extractSection(document, wantedHeading) {
// ATX Headings as defined in GitHub Flavored Markdown (https://github.github.com/gfm/#atx-headings)
const heading = /^ {0,3}(?<lead>#{1,6})(?: [ \t\v\f]*(?<text>.*?)[ \t\v\f]*)?(?:[\n\r]+|$)/gm;
const wantedHeadingRe = makeWordRegExp(wantedHeading);
let start, end;
for (const m of document.matchAll(heading)) {
if (!start) {
if (m.groups.text.search(wantedHeadingRe) === 0) {
start = m;
}
} else if (m.groups.lead.length <= start.groups.lead.length) {
end = m;
break;
}
}
if (start) {
return document.slice(start.index + start[0].length, end?.index);
}
}

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
CHECKSUMS="$RUNNER_TEMP/checksums.txt"
# Extract tarball content into a tmp directory
tar xf "$TARBALL" -C "$RUNNER_TEMP"
# Move to extracted directory
cd "$RUNNER_TEMP/package"
# Checksum all Solidity files
find . -type f -name "*.sol" | xargs shasum > "$CHECKSUMS"
# Back to directory with git contents
cd "$GITHUB_WORKSPACE/contracts"
# Check against tarball contents
shasum -c "$CHECKSUMS"

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
dist_tag() {
PACKAGE_JSON_NAME="$(jq -r .name ./package.json)"
LATEST_NPM_VERSION="$(npm info "$PACKAGE_JSON_NAME" version)"
PACKAGE_JSON_VERSION="$(jq -r .version ./package.json)"
if [ "$PRERELEASE" = "true" ]; then
echo "next"
elif npx semver -r ">$LATEST_NPM_VERSION" "$PACKAGE_JSON_VERSION" > /dev/null; then
echo "latest"
else
# This is a patch for an older version
# npm can't publish without a tag
echo "tmp"
fi
}
cd contracts
TARBALL="$(npm pack | tee /dev/stderr | tail -1)"
echo "tarball_name=$TARBALL" >> $GITHUB_OUTPUT
echo "tarball=$(pwd)/$TARBALL" >> $GITHUB_OUTPUT
echo "tag=$(dist_tag)" >> $GITHUB_OUTPUT
cd ..

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
PACKAGE_JSON_NAME="$(tar xfO "$TARBALL" package/package.json | jq -r .name)"
PACKAGE_JSON_VERSION="$(tar xfO "$TARBALL" package/package.json | jq -r .version)"
# Intentionally escape $ to avoid interpolation and writing the token to disk
echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc
# Actual publish
npm publish "$TARBALL" --tag "$TAG"
# Clean up tags
delete_tag() {
npm dist-tag rm "$PACKAGE_JSON_NAME" "$1"
}
if [ "$TAG" = tmp ]; then
delete_tag "$TAG"
elif [ "$TAG" = latest ]; then
# Delete the next tag if it exists and is a prerelease for what is currently being published
if npm dist-tag ls "$PACKAGE_JSON_NAME" | grep -q "next: $PACKAGE_JSON_VERSION"; then
delete_tag next
fi
fi

View File

@@ -0,0 +1,7 @@
module.exports = ({ github, context }) =>
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'release-cycle.yml',
ref: process.env.REF || process.env.GITHUB_REF_NAME,
});

View File

@@ -0,0 +1,17 @@
const { coerce, inc, rsort } = require('semver');
const { join } = require('path');
const { version } = require(join(__dirname, '../../../package.json'));
module.exports = async ({ core }) => {
// Variables not in the context
const refName = process.env.GITHUB_REF_NAME;
// Compare package.json version's next patch vs. first version patch
// A recently opened branch will give the next patch for the previous minor
// So, we get the max against the patch 0 of the release branch's version
const branchPatch0 = coerce(refName.replace('release-v', '')).version;
const packageJsonNextPatch = inc(version, 'patch');
const [nextVersion] = rsort([branchPatch0, packageJsonNextPatch], false);
core.exportVariable('TITLE', `Release v${nextVersion}`);
};

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
# Set changeset status location
# This is needed because `changeset status --output` only works with relative routes
CHANGESETS_STATUS_JSON="$(realpath --relative-to=. "$RUNNER_TEMP/status.json")"
# Save changeset status to temp file
npx changeset status --output="$CHANGESETS_STATUS_JSON"
# Defensive assertion. SHOULD NOT BE REACHED
if [ "$(jq '.releases | length' "$CHANGESETS_STATUS_JSON")" != 1 ]; then
echo "::error file=$CHANGESETS_STATUS_JSON::The status doesn't contain only 1 release"
exit 1;
fi;
# Create branch
BRANCH_SUFFIX="$(jq -r '.releases[0].newVersion | gsub("\\.\\d+$"; "")' $CHANGESETS_STATUS_JSON)"
RELEASE_BRANCH="release-v$BRANCH_SUFFIX"
git checkout -b "$RELEASE_BRANCH"
# Output branch
echo "branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT
# Enter in prerelease state
npx changeset pre enter rc
git add .
git commit -m "Start release candidate"
# Push branch
if ! git push origin "$RELEASE_BRANCH"; then
echo "::error file=scripts/release/start.sh::Can't push $RELEASE_BRANCH. Did you forget to run this workflow from $RELEASE_BRANCH?"
exit 1
fi

View File

@@ -0,0 +1,112 @@
const { readPreState } = require('@changesets/pre');
const { default: readChangesets } = require('@changesets/read');
const { join } = require('path');
const { fetch } = require('undici');
const { version, name: packageName } = require(join(__dirname, '../../../contracts/package.json'));
module.exports = async ({ github, context, core }) => {
const state = await getState({ github, context, core });
function setOutput(key, value) {
core.info(`State ${key} = ${value}`);
core.setOutput(key, value);
}
// Jobs to trigger
setOutput('start', shouldRunStart(state));
setOutput('promote', shouldRunPromote(state));
setOutput('changesets', shouldRunChangesets(state));
setOutput('publish', shouldRunPublish(state));
setOutput('merge', shouldRunMerge(state));
// Global Variables
setOutput('is_prerelease', state.prerelease);
};
function shouldRunStart({ isMaster, isWorkflowDispatch, botRun }) {
return isMaster && isWorkflowDispatch && !botRun;
}
function shouldRunPromote({ isReleaseBranch, isWorkflowDispatch, botRun }) {
return isReleaseBranch && isWorkflowDispatch && !botRun;
}
function shouldRunChangesets({ isReleaseBranch, isPush, isWorkflowDispatch, botRun }) {
return (isReleaseBranch && isPush) || (isReleaseBranch && isWorkflowDispatch && botRun);
}
function shouldRunPublish({ isReleaseBranch, isPush, hasPendingChangesets, isPublishedOnNpm }) {
return isReleaseBranch && isPush && !hasPendingChangesets && !isPublishedOnNpm;
}
function shouldRunMerge({
isReleaseBranch,
isPush,
prerelease,
isCurrentFinalVersion,
hasPendingChangesets,
prBackExists,
}) {
return isReleaseBranch && isPush && !prerelease && isCurrentFinalVersion && !hasPendingChangesets && !prBackExists;
}
async function getState({ github, context, core }) {
// Variables not in the context
const refName = process.env.GITHUB_REF_NAME;
const botRun = process.env.TRIGGERING_ACTOR === 'github-actions[bot]';
const { changesets, preState } = await readChangesetState();
// Static vars
const state = {
refName,
hasPendingChangesets: changesets.length > 0,
prerelease: preState?.mode === 'pre',
isMaster: refName === 'master',
isReleaseBranch: refName.startsWith('release-v'),
isWorkflowDispatch: context.eventName === 'workflow_dispatch',
isPush: context.eventName === 'push',
isCurrentFinalVersion: !version.includes('-rc.'),
botRun,
};
// Async vars
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:merge/${state.refName}`,
base: 'master',
state: 'open',
});
state.prBackExists = prs.length !== 0;
state.isPublishedOnNpm = await isPublishedOnNpm(packageName, version);
// Log every state value in debug mode
if (core.isDebug()) for (const [key, value] of Object.entries(state)) core.debug(`${key}: ${value}`);
return state;
}
// From https://github.com/changesets/action/blob/v1.4.1/src/readChangesetState.ts
async function readChangesetState(cwd = process.cwd()) {
const preState = await readPreState(cwd);
const isInPreMode = preState !== undefined && preState.mode === 'pre';
let changesets = await readChangesets(cwd);
if (isInPreMode) {
changesets = changesets.filter(x => !preState.changesets.includes(x.id));
}
return {
preState: isInPreMode ? preState : undefined,
changesets,
};
}
async function isPublishedOnNpm(package, version) {
const res = await fetch(`https://registry.npmjs.com/${package}/${version}`);
return res.ok;
}