Skip to content

Commit 575a99d

Browse files
committed
Create Github workflows for release process
1 parent 9f7f02a commit 575a99d

File tree

16 files changed

+452
-2
lines changed

16 files changed

+452
-2
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# D: Manual Dispatch
2+
# M: Merge release PR
3+
# C: Commit
4+
# ┌───────────┐ ┌─────────────┐ ┌────────────────┐
5+
# │Development├──D──►RC-Unreleased│ ┌──►Final-Unreleased│
6+
# └───────────┘ └─┬─────────▲─┘ │ └─┬────────────▲─┘
7+
# │ │ │ │ │
8+
# M C D M C
9+
# │ │ │ │ │
10+
# ┌▼─────────┴┐ │ ┌▼────────────┴┐
11+
# │RC-Released├───┘ │Final-Released│
12+
# └───────────┘ └──────────────┘
13+
name: Release Cycle
14+
15+
on:
16+
push:
17+
branches:
18+
- release-v*
19+
workflow_dispatch: {}
20+
21+
concurrency: ${{ github.workflow }}-${{ github.ref }}
22+
23+
jobs:
24+
state:
25+
name: Check state
26+
permissions:
27+
pull-requests: read
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v4
31+
with:
32+
submodules: recursive
33+
- name: Set up environment
34+
uses: ./.github/actions/setup
35+
- id: state
36+
name: Get state
37+
uses: actions/github-script@v7
38+
env:
39+
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
40+
with:
41+
result-encoding: string
42+
script: await require('./scripts/release/workflow/state.js')({ github, context, core })
43+
outputs:
44+
# Job Flags
45+
start: ${{ steps.state.outputs.start }}
46+
changesets: ${{ steps.state.outputs.changesets }}
47+
promote: ${{ steps.state.outputs.promote }}
48+
publish: ${{ steps.state.outputs.publish }}
49+
merge: ${{ steps.state.outputs.merge }}
50+
51+
# Global variables
52+
is_prerelease: ${{ steps.state.outputs.is_prerelease }}
53+
54+
start:
55+
needs: state
56+
name: Start new release candidate
57+
permissions:
58+
contents: write
59+
actions: write
60+
if: needs.state.outputs.start == 'true'
61+
runs-on: ubuntu-latest
62+
steps:
63+
- uses: actions/checkout@v4
64+
with:
65+
submodules: recursive
66+
- name: Set up environment
67+
uses: ./.github/actions/setup
68+
- run: bash scripts/git-user-config.sh
69+
- id: start
70+
name: Create branch with release candidate
71+
run: bash scripts/release/workflow/start.sh
72+
- name: Re-run workflow
73+
uses: actions/github-script@v7
74+
env:
75+
REF: ${{ steps.start.outputs.branch }}
76+
with:
77+
script: await require('./scripts/release/workflow/rerun.js')({ github, context })
78+
79+
promote:
80+
needs: state
81+
name: Promote to final release
82+
permissions:
83+
contents: write
84+
actions: write
85+
if: needs.state.outputs.promote == 'true'
86+
runs-on: ubuntu-latest
87+
steps:
88+
- uses: actions/checkout@v4
89+
with:
90+
submodules: recursive
91+
- name: Set up environment
92+
uses: ./.github/actions/setup
93+
- run: bash scripts/git-user-config.sh
94+
- name: Exit prerelease state
95+
if: needs.state.outputs.is_prerelease == 'true'
96+
run: bash scripts/release/workflow/exit-prerelease.sh
97+
- name: Re-run workflow
98+
uses: actions/github-script@v7
99+
with:
100+
script: await require('./scripts/release/workflow/rerun.js')({ github, context })
101+
102+
changesets:
103+
needs: state
104+
name: Update PR to release
105+
permissions:
106+
contents: write
107+
pull-requests: write
108+
if: needs.state.outputs.changesets == 'true'
109+
runs-on: ubuntu-latest
110+
steps:
111+
- uses: actions/checkout@v4
112+
with:
113+
fetch-depth: 0 # To get all tags
114+
submodules: recursive
115+
- name: Set up environment
116+
uses: ./.github/actions/setup
117+
- name: Set release title
118+
uses: actions/github-script@v7
119+
with:
120+
result-encoding: string
121+
script: await require('./scripts/release/workflow/set-changesets-pr-title.js')({ core })
122+
- name: Create PR
123+
uses: changesets/action@v1
124+
env:
125+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126+
PRERELEASE: ${{ needs.state.outputs.is_prerelease }}
127+
with:
128+
version: npm run version
129+
title: ${{ env.TITLE }}
130+
commit: ${{ env.TITLE }}
131+
body: | # Wait for support on this https://github.com/changesets/action/pull/250
132+
This is an automated PR for releasing ${{ github.repository }}
133+
Check [CHANGELOG.md](${{ github.repository }}/CHANGELOG.md)
134+
135+
publish:
136+
needs: state
137+
name: Publish to npm
138+
environment: npm
139+
permissions:
140+
contents: write
141+
id-token: write
142+
if: needs.state.outputs.publish == 'true'
143+
runs-on: ubuntu-latest
144+
steps:
145+
- uses: actions/checkout@v4
146+
with:
147+
submodules: recursive
148+
- name: Set up environment
149+
uses: ./.github/actions/setup
150+
- id: pack
151+
name: Pack
152+
run: bash scripts/release/workflow/pack.sh
153+
env:
154+
PRERELEASE: ${{ needs.state.outputs.is_prerelease }}
155+
- name: Upload tarball artifact
156+
uses: actions/upload-artifact@v4
157+
with:
158+
name: ${{ github.ref_name }}
159+
path: ${{ steps.pack.outputs.tarball }}
160+
- name: Publish
161+
run: bash scripts/release/workflow/publish.sh
162+
env:
163+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
164+
TARBALL: ${{ steps.pack.outputs.tarball }}
165+
TAG: ${{ steps.pack.outputs.tag }}
166+
NPM_CONFIG_PROVENANCE: true
167+
- name: Create Github Release
168+
uses: actions/github-script@v7
169+
env:
170+
PRERELEASE: ${{ needs.state.outputs.is_prerelease }}
171+
with:
172+
script: await require('./scripts/release/workflow/github-release.js')({ github, context })
173+
outputs:
174+
tarball_name: ${{ steps.pack.outputs.tarball_name }}
175+
176+
integrity_check:
177+
needs: publish
178+
name: Tarball Integrity Check
179+
runs-on: ubuntu-latest
180+
steps:
181+
- uses: actions/checkout@v4
182+
with:
183+
submodules: recursive
184+
- name: Download tarball artifact
185+
id: artifact
186+
uses: actions/download-artifact@v4
187+
with:
188+
name: ${{ github.ref_name }}
189+
- name: Check integrity
190+
run: bash scripts/release/workflow/integrity-check.sh
191+
env:
192+
TARBALL: ${{ steps.artifact.outputs.download-path }}/${{ needs.publish.outputs.tarball_name }}
193+
194+
merge:
195+
needs: state
196+
name: Create PR back to master
197+
permissions:
198+
contents: write
199+
pull-requests: write
200+
if: needs.state.outputs.merge == 'true'
201+
runs-on: ubuntu-latest
202+
env:
203+
MERGE_BRANCH: merge/${{ github.ref_name }}
204+
steps:
205+
- uses: actions/checkout@v4
206+
with:
207+
fetch-depth: 0 # All branches
208+
submodules: recursive
209+
- name: Set up environment
210+
uses: ./.github/actions/setup
211+
- run: bash scripts/git-user-config.sh
212+
- name: Create branch to merge
213+
run: |
214+
git checkout -B "$MERGE_BRANCH" "$GITHUB_REF_NAME"
215+
git push -f origin "$MERGE_BRANCH"
216+
- name: Create PR back to master
217+
uses: actions/github-script@v7
218+
with:
219+
script: |
220+
await github.rest.pulls.create({
221+
owner: context.repo.owner,
222+
repo: context.repo.repo,
223+
head: process.env.MERGE_BRANCH,
224+
base: 'master',
225+
title: '${{ format('Merge {0} branch', github.ref_name) }}'
226+
});

contracts/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
"repository": {
1515
"type": "git",
16-
"url": "https://github.com/OpenZeppelin/openzeppelin-contracts-confidential.git"
16+
"url": "https://github.com/OpenZeppelin/openzeppelin-confidential-contracts.git"
1717
},
1818
"keywords": [
1919
"solidity",
@@ -28,10 +28,11 @@
2828
"author": "OpenZeppelin Community <maintainers@openzeppelin.org>",
2929
"license": "MIT",
3030
"bugs": {
31-
"url": "https://github.com/OpenZeppelin/openzeppelin-contracts-confidential/issues"
31+
"url": "https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/issues"
3232
},
3333
"homepage": "https://openzeppelin.com/contracts/",
3434
"peerDependencies": {
35+
"@fhevm/solidity": "0.7.0",
3536
"@openzeppelin/contracts": "5.3.0"
3637
}
3738
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
"lint:js:fix": "prettier --loglevel warn '**/*.{js,ts}' --write && eslint . --fix",
2020
"lint:sol": "prettier --loglevel warn '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'",
2121
"lint:sol:fix": "prettier --loglevel warn '{contracts,test}/**/*.sol' --write",
22+
"prepack": "scripts/prepack.sh",
2223
"prepare": "husky",
2324
"prepare-docs": "scripts/prepare-docs.sh",
25+
"version": "scripts/release/version.sh",
2426
"test": "hardhat test",
2527
"test:gas": "REPORT_GAS=true hardhat test",
2628
"test:generation": "scripts/checks/generation.sh",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../lib/openzeppelin-contracts/scripts/release/format-changelog.js
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../lib/openzeppelin-contracts/scripts/release/synchronize-versions.js

scripts/release/update-comment.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env node
2+
const fs = require('fs');
3+
const proc = require('child_process');
4+
const semver = require('semver');
5+
const run = (cmd, ...args) => proc.execFileSync(cmd, args, { encoding: 'utf8' }).trim();
6+
7+
const gitStatus = run('git', 'status', '--porcelain', '-uno', 'contracts/**/*.sol');
8+
if (gitStatus.length > 0) {
9+
console.error('Contracts directory is not clean');
10+
process.exit(1);
11+
}
12+
13+
const { version } = require('../../package.json');
14+
15+
// Get latest tag according to semver.
16+
const [tag] = run('git', 'tag')
17+
.split(/\r?\n/)
18+
.filter(semver.coerce) // check version can be processed
19+
.filter(v => semver.satisfies(v, `< ${version}`)) // ignores prereleases unless currently a prerelease
20+
.sort(semver.rcompare);
21+
22+
// Ordering tag → HEAD is important here.
23+
const files = run('git', 'diff', tag, 'HEAD', '--name-only', 'contracts/**/*.sol')
24+
.split(/\r?\n/)
25+
.filter(file => file && !file.match(/mock/i) && fs.existsSync(file));
26+
27+
for (const file of files) {
28+
const current = fs.readFileSync(file, 'utf8');
29+
const updated = current.replace(
30+
/(\/\/ SPDX-License-Identifier:.*)$(\n\/\/ OpenZeppelin Confidential Contracts .*$)?/m,
31+
`$1\n// OpenZeppelin Confidential Contracts (last updated v${version}) (${file.replace('contracts/', '')})`,
32+
);
33+
``;
34+
fs.writeFileSync(file, updated);
35+
}

scripts/release/version.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../lib/openzeppelin-contracts/scripts/release/version.sh
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../lib/openzeppelin-contracts/scripts/release/workflow/exit-prerelease.sh
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const { readFileSync } = require('fs');
2+
const { join } = require('path');
3+
const { version } = require(join(__dirname, '../../../package.json'));
4+
5+
module.exports = async ({ github, context }) => {
6+
const changelog = readFileSync('CHANGELOG.md', 'utf8');
7+
8+
await github.rest.repos.createRelease({
9+
owner: context.repo.owner,
10+
repo: context.repo.repo,
11+
tag_name: `v${version}`,
12+
target_commitish: github.ref_name,
13+
body: extractSection(changelog, version),
14+
prerelease: process.env.PRERELEASE === 'true',
15+
});
16+
};
17+
18+
// From https://github.com/frangio/extract-changelog/blob/master/src/utils/word-regexp.ts
19+
function makeWordRegExp(word) {
20+
const start = word.length > 0 && /\b/.test(word[0]) ? '\\b' : '';
21+
const end = word.length > 0 && /\b/.test(word[word.length - 1]) ? '\\b' : '';
22+
return new RegExp(start + [...word].map(c => (/[a-z0-9]/i.test(c) ? c : '\\' + c)).join('') + end);
23+
}
24+
25+
// From https://github.com/frangio/extract-changelog/blob/master/src/core.ts
26+
function extractSection(document, wantedHeading) {
27+
// ATX Headings as defined in GitHub Flavored Markdown (https://github.github.com/gfm/#atx-headings)
28+
const heading = /^ {0,3}(?<lead>#{1,6})(?: [ \t\v\f]*(?<text>.*?)[ \t\v\f]*)?(?:[\n\r]+|$)/gm;
29+
30+
const wantedHeadingRe = makeWordRegExp(wantedHeading);
31+
32+
let start, end;
33+
34+
for (const m of document.matchAll(heading)) {
35+
if (!start) {
36+
if (m.groups.text.search(wantedHeadingRe) === 0) {
37+
start = m;
38+
}
39+
} else if (m.groups.lead.length <= start.groups.lead.length) {
40+
end = m;
41+
break;
42+
}
43+
}
44+
45+
if (start) {
46+
return document.slice(start.index + start[0].length, end?.index);
47+
}
48+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../lib/openzeppelin-contracts/scripts/release/workflow/integrity-check.sh

0 commit comments

Comments
 (0)