mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-26 22:56:43 +00:00
Add ci deploy with hardhat (#5)
* Add deploy workflow with hardhat * Rm hardhat deployments dir * Add contract deploy script * Add dev goodies for quick use during development * Update deps * Use custom script for contract deployment * Add info about deploy.ts * Add step to show docker logs after contract deployment * Add building of new image with deployed contract * Add dynamic wait for geth to be up instead of sleep * Print container logs while waiting for geth * Update container cmd * dbg * dbg * Update args * Fix push image * dbg * Fix build command typo * Update README * Update README * Allow deploy workflow to run only when PR is merged in master and has label CI:Deploy
This commit is contained in:
parent
a6ac032f7a
commit
7df5aa3c92
15 changed files with 10122 additions and 1 deletions
80
.github/workflows/deploy.yml
vendored
Normal file
80
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
name: deploy
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [master]
|
||||
paths: ["**"]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CI:Deploy')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: prep tags
|
||||
id: vars
|
||||
run: |
|
||||
echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
echo "IMAGE_NAME=${{ secrets.DOCKERHUB_USERNAME }}/geth-dev" >> $GITHUB_ENV
|
||||
|
||||
- name: start geth
|
||||
run: |
|
||||
mkdir -p geth_data
|
||||
docker run -d --name geth-service -p 8545:8545 -p 30303:30303 -v "$(pwd)/geth_data:/root/.ethereum" yyx00xzz/geth-dev:latest --dev --http --http.addr 0.0.0.0 --http.port 8545 --http.api admin,debug,web3,eth,txpool,miner,net,dev --miner.gasprice "1" --http.corsdomain "*"
|
||||
echo "Waiting for Geth to be ready..."
|
||||
# Poll the Geth RPC endpoint until it's responsive
|
||||
while ! curl -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' http://localhost:8545 > /dev/null; do
|
||||
echo "Geth not ready yet, waiting..."
|
||||
docker logs geth-service
|
||||
sleep 5
|
||||
done
|
||||
echo "Geth is ready."
|
||||
|
||||
- name: install hardhat deps
|
||||
run: |
|
||||
cd ./hardhat
|
||||
npm install
|
||||
|
||||
- name: deploy contract
|
||||
run: |
|
||||
cd ./hardhat
|
||||
npx hardhat compile
|
||||
npx hardhat run scripts/deploy.ts --network geth
|
||||
|
||||
- name: get docker logs
|
||||
run: docker logs geth-service
|
||||
|
||||
- name: stop geth
|
||||
run: docker stop geth-service
|
||||
|
||||
- name: build new image with deployed contract
|
||||
run: |
|
||||
pwd
|
||||
|
||||
cat <<EOF > Dockerfile.with-contract
|
||||
FROM yyx00xzz/geth-dev:latest
|
||||
COPY geth_data /root/.ethereum
|
||||
EOF
|
||||
|
||||
docker build -t $IMAGE_NAME:with-contract-latest -t $IMAGE_NAME:with-contract-$COMMIT_SHA -f Dockerfile.with-contract .
|
||||
|
||||
rm Dockerfile.with-contract
|
||||
|
||||
docker images -a
|
||||
- name: push image
|
||||
run: |
|
||||
docker push $IMAGE_NAME:with-contract-latest
|
||||
docker push $IMAGE_NAME:with-contract-$COMMIT_SHA
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
|
|
@ -56,3 +56,21 @@ cmd/evm/evm
|
|||
cmd/geth/geth
|
||||
cmd/rlpdump/rlpdump
|
||||
cmd/workload/workload
|
||||
|
||||
hardhat/node_modules
|
||||
hardhat/.env
|
||||
|
||||
# Hardhat files
|
||||
hardhat/cache
|
||||
hardhat/artifacts
|
||||
|
||||
# TypeChain files
|
||||
hardhat/typechain
|
||||
hardhat/typechain-types
|
||||
|
||||
# solidity-coverage files
|
||||
hardhat/coverage
|
||||
hardhat/coverage.json
|
||||
|
||||
# Hardhat Ignition default folder for deployments against a local node
|
||||
hardhat/ignition/deployments/chain-31337
|
||||
|
|
|
|||
23
README.md
23
README.md
|
|
@ -1,9 +1,32 @@
|
|||
## DevOps docs
|
||||
All workflows are limited to PRs with target=master for the sake of simplicity in this demo.
|
||||
In the future, a feature should be implemented which allows to build images and deploy contracts from feature branches.
|
||||
|
||||
### Build
|
||||
When a PR with label CI:Build is merged in the master branch, a Docker image is build and pushed tagged with latest AND with the commit ID. This way you can refer to older version if needed but at the same time latest is available.
|
||||
|
||||
!NB If you encounter a problem with pushing to the registry, check if the access token has not expired. It has 30 days expiration.
|
||||
|
||||
### Deploy
|
||||
|
||||
#### Deploying the contract
|
||||
Although hardhat ignition works perfectly with the hardhat network, there are issues when trying to deploy to Geth.
|
||||
|
||||
To solve this, there is deploy script in `hardhat/scripts` dir which deploys the sample Lock contract to the blockchain.
|
||||
|
||||
If you need to deploy the contract locally:
|
||||
1. cd to `hardhat` dir
|
||||
2. run `npx hardhat run scripts/deploy.ts --network geth`
|
||||
|
||||
---
|
||||
|
||||
#### Creating new image containing the contract
|
||||
I've decided to go with mounted dir for the data dir because it is easier to create new dokcer image with the contract already deployed. If I've stayed on the volume mount, it would require more steps to persist the data dir.
|
||||
Using mounted dir for the data can be tricky because of permission errs tho.
|
||||
|
||||
Currently, the new image (with contract) is built from the latest base image.
|
||||
This should be extended in the future to support building from a given tag.
|
||||
|
||||
## Go Ethereum
|
||||
|
||||
Golang execution layer implementation of the Ethereum protocol.
|
||||
|
|
|
|||
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
geth:
|
||||
image: yyx00xzz/geth-dev:latest
|
||||
ports:
|
||||
- "8545:8545"
|
||||
- "30303:30303"
|
||||
command: >
|
||||
--dev
|
||||
--http
|
||||
--http.addr 0.0.0.0
|
||||
--http.port 8545
|
||||
--http.api admin,debug,web3,eth,txpool,miner,net,dev
|
||||
--miner.gasprice "1"
|
||||
--http.corsdomain "*"
|
||||
volumes:
|
||||
- geth_data:/root/.ethereum
|
||||
|
||||
volumes:
|
||||
geth_data:
|
||||
13
hardhat/README.md
Normal file
13
hardhat/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Sample Hardhat Project
|
||||
|
||||
This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a Hardhat Ignition module that deploys that contract.
|
||||
|
||||
Try running some of the following tasks:
|
||||
|
||||
```shell
|
||||
npx hardhat help
|
||||
npx hardhat test
|
||||
REPORT_GAS=true npx hardhat test
|
||||
npx hardhat node
|
||||
npx hardhat ignition deploy ./ignition/modules/Lock.ts
|
||||
```
|
||||
34
hardhat/contracts/Lock.sol
Normal file
34
hardhat/contracts/Lock.sol
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.28;
|
||||
|
||||
// Uncomment this line to use console.log
|
||||
// import "hardhat/console.sol";
|
||||
|
||||
contract Lock {
|
||||
uint public unlockTime;
|
||||
address payable public owner;
|
||||
|
||||
event Withdrawal(uint amount, uint when);
|
||||
|
||||
constructor(uint _unlockTime) payable {
|
||||
require(
|
||||
block.timestamp < _unlockTime,
|
||||
"Unlock time should be in the future"
|
||||
);
|
||||
|
||||
unlockTime = _unlockTime;
|
||||
owner = payable(msg.sender);
|
||||
}
|
||||
|
||||
function withdraw() public {
|
||||
// Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal
|
||||
// console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp);
|
||||
|
||||
require(block.timestamp >= unlockTime, "You can't withdraw yet");
|
||||
require(msg.sender == owner, "You aren't the owner");
|
||||
|
||||
emit Withdrawal(address(this).balance, block.timestamp);
|
||||
|
||||
owner.transfer(address(this).balance);
|
||||
}
|
||||
}
|
||||
14
hardhat/dev-goodies/extract_pk.js
Normal file
14
hardhat/dev-goodies/extract_pk.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const Web3 = require('web3');
|
||||
const fs = require('fs');
|
||||
|
||||
// Load the keystore file
|
||||
const keyfile = fs.readFileSync('Full path to JSON key file', 'utf-8');
|
||||
|
||||
// Your password
|
||||
const password = 'password';
|
||||
|
||||
const web3 = new Web3();
|
||||
const account = web3.eth.accounts.decrypt(JSON.parse(keyfile), password);
|
||||
|
||||
// Print the private key
|
||||
console.log('Private Key:', account.privateKey);
|
||||
7
hardhat/dev-goodies/generate_pk.js
Normal file
7
hardhat/dev-goodies/generate_pk.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const { ethers } = require("ethers");
|
||||
|
||||
// Create a random wallet
|
||||
const wallet = ethers.Wallet.createRandom();
|
||||
|
||||
console.log("Private Key:", wallet.privateKey);
|
||||
console.log("Address:", wallet.address);
|
||||
14
hardhat/hardhat.config.ts
Normal file
14
hardhat/hardhat.config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { HardhatUserConfig } from "hardhat/config";
|
||||
import "@nomicfoundation/hardhat-toolbox";
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
solidity: "0.8.28",
|
||||
networks: {
|
||||
geth: {
|
||||
url: "http://127.0.0.1:8545",
|
||||
chainId: 1337
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
20
hardhat/ignition/modules/Lock.ts
Normal file
20
hardhat/ignition/modules/Lock.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// This setup uses Hardhat Ignition to manage smart contract deployments.
|
||||
// Learn more about it at https://hardhat.org/ignition
|
||||
|
||||
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
|
||||
|
||||
const JAN_1ST_2030 = 1893456000;
|
||||
const ONE_GWEI: bigint = 1_000_000_000n;
|
||||
|
||||
const LockModule = buildModule("LockModule", (m) => {
|
||||
const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030);
|
||||
const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI);
|
||||
|
||||
const lock = m.contract("Lock", [unlockTime], {
|
||||
value: lockedAmount,
|
||||
});
|
||||
|
||||
return { lock };
|
||||
});
|
||||
|
||||
export default LockModule;
|
||||
9695
hardhat/package-lock.json
generated
Normal file
9695
hardhat/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
21
hardhat/package.json
Normal file
21
hardhat/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "hardhat",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@nomicfoundation/hardhat-ethers": "^3.0.8",
|
||||
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
|
||||
"hardhat": "^2.22.19"
|
||||
},
|
||||
"dependencies": {
|
||||
"ethers": "^6.13.5",
|
||||
"web3": "^4.16.0"
|
||||
}
|
||||
}
|
||||
23
hardhat/scripts/deploy.ts
Normal file
23
hardhat/scripts/deploy.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { ethers } from "hardhat";
|
||||
|
||||
async function main() {
|
||||
const unlockTime = Math.floor(Date.now() / 1000) + 60;
|
||||
const lockedAmount = ethers.parseEther("0.01");
|
||||
|
||||
const Lock = await ethers.getContractFactory("Lock");
|
||||
const lock = await Lock.deploy(unlockTime, { value: lockedAmount });
|
||||
|
||||
await lock.waitForDeployment();
|
||||
const lockAddress = await lock.getAddress(); // Use getAddress() for type safety
|
||||
|
||||
console.log(
|
||||
`Lock with ${ethers.formatEther(
|
||||
await ethers.provider.getBalance(lockAddress)
|
||||
)} ETH and unlock timestamp ${unlockTime} deployed to ${lockAddress}`
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
127
hardhat/test/Lock.ts
Normal file
127
hardhat/test/Lock.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import {
|
||||
time,
|
||||
loadFixture,
|
||||
} from "@nomicfoundation/hardhat-toolbox/network-helpers";
|
||||
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
|
||||
import { expect } from "chai";
|
||||
import hre from "hardhat";
|
||||
|
||||
describe("Lock", function () {
|
||||
// We define a fixture to reuse the same setup in every test.
|
||||
// We use loadFixture to run this setup once, snapshot that state,
|
||||
// and reset Hardhat Network to that snapshot in every test.
|
||||
async function deployOneYearLockFixture() {
|
||||
const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
|
||||
const ONE_GWEI = 1_000_000_000;
|
||||
|
||||
const lockedAmount = ONE_GWEI;
|
||||
const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;
|
||||
|
||||
// Contracts are deployed using the first signer/account by default
|
||||
const [owner, otherAccount] = await hre.ethers.getSigners();
|
||||
|
||||
const Lock = await hre.ethers.getContractFactory("Lock");
|
||||
const lock = await Lock.deploy(unlockTime, { value: lockedAmount });
|
||||
|
||||
return { lock, unlockTime, lockedAmount, owner, otherAccount };
|
||||
}
|
||||
|
||||
describe("Deployment", function () {
|
||||
it("Should set the right unlockTime", async function () {
|
||||
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);
|
||||
|
||||
expect(await lock.unlockTime()).to.equal(unlockTime);
|
||||
});
|
||||
|
||||
it("Should set the right owner", async function () {
|
||||
const { lock, owner } = await loadFixture(deployOneYearLockFixture);
|
||||
|
||||
expect(await lock.owner()).to.equal(owner.address);
|
||||
});
|
||||
|
||||
it("Should receive and store the funds to lock", async function () {
|
||||
const { lock, lockedAmount } = await loadFixture(
|
||||
deployOneYearLockFixture
|
||||
);
|
||||
|
||||
expect(await hre.ethers.provider.getBalance(lock.target)).to.equal(
|
||||
lockedAmount
|
||||
);
|
||||
});
|
||||
|
||||
it("Should fail if the unlockTime is not in the future", async function () {
|
||||
// We don't use the fixture here because we want a different deployment
|
||||
const latestTime = await time.latest();
|
||||
const Lock = await hre.ethers.getContractFactory("Lock");
|
||||
await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith(
|
||||
"Unlock time should be in the future"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Withdrawals", function () {
|
||||
describe("Validations", function () {
|
||||
it("Should revert with the right error if called too soon", async function () {
|
||||
const { lock } = await loadFixture(deployOneYearLockFixture);
|
||||
|
||||
await expect(lock.withdraw()).to.be.revertedWith(
|
||||
"You can't withdraw yet"
|
||||
);
|
||||
});
|
||||
|
||||
it("Should revert with the right error if called from another account", async function () {
|
||||
const { lock, unlockTime, otherAccount } = await loadFixture(
|
||||
deployOneYearLockFixture
|
||||
);
|
||||
|
||||
// We can increase the time in Hardhat Network
|
||||
await time.increaseTo(unlockTime);
|
||||
|
||||
// We use lock.connect() to send a transaction from another account
|
||||
await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith(
|
||||
"You aren't the owner"
|
||||
);
|
||||
});
|
||||
|
||||
it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () {
|
||||
const { lock, unlockTime } = await loadFixture(
|
||||
deployOneYearLockFixture
|
||||
);
|
||||
|
||||
// Transactions are sent using the first signer by default
|
||||
await time.increaseTo(unlockTime);
|
||||
|
||||
await expect(lock.withdraw()).not.to.be.reverted;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function () {
|
||||
it("Should emit an event on withdrawals", async function () {
|
||||
const { lock, unlockTime, lockedAmount } = await loadFixture(
|
||||
deployOneYearLockFixture
|
||||
);
|
||||
|
||||
await time.increaseTo(unlockTime);
|
||||
|
||||
await expect(lock.withdraw())
|
||||
.to.emit(lock, "Withdrawal")
|
||||
.withArgs(lockedAmount, anyValue); // We accept any value as `when` arg
|
||||
});
|
||||
});
|
||||
|
||||
describe("Transfers", function () {
|
||||
it("Should transfer the funds to the owner", async function () {
|
||||
const { lock, unlockTime, lockedAmount, owner } = await loadFixture(
|
||||
deployOneYearLockFixture
|
||||
);
|
||||
|
||||
await time.increaseTo(unlockTime);
|
||||
|
||||
await expect(lock.withdraw()).to.changeEtherBalances(
|
||||
[owner, lock],
|
||||
[lockedAmount, -lockedAmount]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
11
hardhat/tsconfig.json
Normal file
11
hardhat/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue