Skip to content

Commit

Permalink
Github Action (#1)
Browse files Browse the repository at this point in the history
* basic scaffolding for github action

* Add build action step

* Get rid of runtime dep on bun

* Find pending spells

* Extract common logic from CLI and action

* Expose public tenderly URLs

* Post github comment
  • Loading branch information
krzkaczor authored Jul 25, 2024
1 parent c068bf2 commit 898ba65
Show file tree
Hide file tree
Showing 18 changed files with 194 additions and 52 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Spell Caster
# Spark Spells GitHub Action

Execute a spell on a persisted, forked network with a shareable link.

Expand Down
24 changes: 24 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Spell Caster
description: Spark Spell Caster

inputs:
github-token:
description: 'Github token is needed to post comments on the PR'
required: true
TENDERLY_ACCOUNT:
description: 'Tenderly credentials'
required: true
TENDERLY_PROJECT:
description: 'Tenderly credentials'
required: true
TENDERLY_API_KEY:
description: 'Tenderly credentials'
required: true

outputs:
time:
description: The time we greeted you

runs:
using: node20
main: out/action.js
13 changes: 1 addition & 12 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,7 @@
"$schema": "https://biomejs.dev/schemas/1.8.1/schema.json",
"files": {
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.json", "README.md"],
"ignore": [
"**/node_modules",
"**/.vscode",
"**/package.json",
"**/dist/**",
"**/.vite",
"packages/app/storybook-static",
"packages/app/__screenshots__",
"packages/app/playwright-report",
"packages/app/test-results",
"src/test/e2e/hars/**"
]
"ignore": ["**/node_modules", "**/.vscode", "**/package.json", "**/out/**"]
},
"formatter": {
"enabled": true,
Expand Down
Binary file modified bun.lockb
Binary file not shown.
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"check:fix": "biome check --write --unsafe .",
"typecheck": "tsc --noEmit",
"test": "bun test",
"fix": "bun run check:fix && bun run test && bun run typecheck"
"fix": "bun run check:fix && bun run test && bun run typecheck",
"build:action": "bun build src/bin/action.ts --outdir=out --target=node"
},
"devDependencies": {
"@types/bun": "latest"
Expand All @@ -16,8 +17,15 @@
"typescript": "^5.0.0"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
"@biomejs/biome": "^1.8.3",
"@superactions/comment": "^0.1.1",
"dax-sh": "^0.41.0",
"dedent": "^1.5.3",
"fetch-retry": "^6.0.0",
"glob": "^11.0.0",
"markdown-table": "^3.0.3",
"viem": "^2.16.5"
}
}
55 changes: 55 additions & 0 deletions src/bin/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import assert from 'node:assert'
import core from '@actions/core'
import github from '@actions/github'
import { createCommentOrUpdate } from '@superactions/comment'
import dedent from 'dedent'
import { markdownTable } from 'markdown-table'
import { ForkAndExecuteSpellReturn, forkAndExecuteSpell } from '..'
import { getConfig } from '../config'
import { getRequiredGithubInput } from '../config/environments/action'
import { findPendingSpells } from '../spells/findPendingSpells'

async function main(): Promise<void> {
const config = getConfig(getRequiredGithubInput)

const allPendingSpellNames = findPendingSpells(process.cwd())
core.info(`Pending spells: ${allPendingSpellNames.join(', ')}`)

const results = await Promise.all(allPendingSpellNames.map((spellName) => forkAndExecuteSpell(spellName, config)))

await postGithubComment(results)

core.info(`Results: ${JSON.stringify(results)}`)
}

await main().catch((error) => {
core.setFailed(error)
})

const uniqueAppId = 'spark-spells-action'
async function postGithubComment(results: ForkAndExecuteSpellReturn[]): Promise<void> {
const now = new Date().toISOString()
const sha = getPrSha()
const table = [
['Spell', 'App URL', 'RPC URL'],
...results.map((result) => [result.spellName, `[🎇 App](${result.appUrl})`, `[🌎 RPC](${result.forkRpc})`]),
]
const message = dedent`
## Spell Caster
Inspect the impact of spell execution on forked networks:
${markdownTable(table)}
<sub>Deployed from ${sha} on ${now}</sub>
`
await createCommentOrUpdate({ githubToken: core.getInput('github-token'), message, uniqueAppId: uniqueAppId })
}

function getPrSha(): string {
const context = github.context

assert(context.eventName === 'pull_request', 'This action can only be run on pull requests')

return context.payload.pull_request!.head.sha
}
19 changes: 19 additions & 0 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import assert from 'node:assert'
import { forkAndExecuteSpell } from '..'
import { getConfig } from '../config'
import { getRequiredShellEnv } from '../config/environments/cli'

async function main(spellName?: string) {
assert(spellName, 'Pass spell name as an argument ex. SparkEthereum_20240627')

const config = getConfig(getRequiredShellEnv)

console.log(`Executing spell ${spellName}`)
const { forkRpc, appUrl } = await forkAndExecuteSpell(spellName, config)

console.log(`Fork Network RPC: ${forkRpc}`)
console.log(`Spark App URL: ${appUrl}`)
}

const arg1 = process.argv[2]
await main(arg1)
8 changes: 8 additions & 0 deletions src/config/environments/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import assert from 'node:assert'
import core from '@actions/core'

export function getRequiredGithubInput(key: string): string {
const value = core.getInput(key)
assert(value, `Missing required github input: ${key}`)
return value
}
2 changes: 1 addition & 1 deletion src/config/env.ts → src/config/environments/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert'

export function getRequiredEnv(key: string): string {
export function getRequiredShellEnv(key: string): string {
const value = process.env[key]
assert(value, `Missing required environment variable: ${key}`)
return value
Expand Down
13 changes: 7 additions & 6 deletions src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Address } from 'viem'
import { type Address, zeroAddress } from 'viem'
import { gnosis, mainnet } from 'viem/chains'
import { getRequiredEnv } from './env'

export interface Config {
tenderly: TenderlyConfig
networks: Record<string, NetworkConfig>
deployer: Address
}

export interface NetworkConfig {
Expand All @@ -19,12 +19,12 @@ export interface TenderlyConfig {
apiKey: string
}

export function getConfig(): Config {
export function getConfig(getEnvVariable: (key: string) => string): Config {
return {
tenderly: {
account: getRequiredEnv('TENDERLY_ACCOUNT'),
project: getRequiredEnv('TENDERLY_PROJECT'),
apiKey: getRequiredEnv('TENDERLY_API_KEY'),
account: getEnvVariable('TENDERLY_ACCOUNT'),
project: getEnvVariable('TENDERLY_PROJECT'),
apiKey: getEnvVariable('TENDERLY_API_KEY'),
},
networks: {
[mainnet.id]: {
Expand All @@ -38,5 +38,6 @@ export function getConfig(): Config {
sparkSpellExecutor: '0xc4218C1127cB24a0D6c1e7D25dc34e10f2625f5A',
},
},
deployer: zeroAddress,
}
}
39 changes: 20 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,44 @@
import assert from 'node:assert'
import { zeroAddress } from 'viem'
import { getConfig } from './config'
import { executeSpell } from './executeSpell'
import { Config } from './config'
import { EthereumClient } from './periphery/ethereum'
import { deployContract } from './periphery/forge'
import { buildAppUrl } from './periphery/spark-app'
import { createTenderlyVNet, getRandomChainId } from './periphery/tenderly'
import { deployContract } from './utils/forge'
import { executeSpell } from './spells/executeSpell'
import { getChainIdFromSpellName } from './utils/getChainIdFromSpellName'

const deployer = zeroAddress

async function main(spellName?: string) {
assert(spellName, 'Pass spell name as an argument ex. SparkEthereum_20240627')
export interface ForkAndExecuteSpellReturn {
spellName: string
originChainId: number
forkRpc: string
forkChainId: number
appUrl: string
}

const config = getConfig()
export async function forkAndExecuteSpell(spellName: string, config: Config): Promise<ForkAndExecuteSpellReturn> {
const originChainId = getChainIdFromSpellName(spellName)
const chain = config.networks[originChainId]
assert(chain, `Chain not found for chainId: ${originChainId}`)
const forkChainId = getRandomChainId()

console.log(`Executing spell ${spellName} on ${chain.name} (chainId=${originChainId})`)

const rpc = await createTenderlyVNet({
account: config.tenderly.account,
apiKey: config.tenderly.apiKey,
project: config.tenderly.project,
originChainId: originChainId,
forkChainId,
})
const ethereumClient = new EthereumClient(rpc, forkChainId, deployer)
const ethereumClient = new EthereumClient(rpc.adminRpcUrl, forkChainId, config.deployer)

const spellAddress = await deployContract(spellName, rpc, deployer)
const spellAddress = await deployContract(spellName, rpc.adminRpcUrl, config.deployer)

await executeSpell({ spellAddress, network: chain, ethereumClient })

console.log(`Fork Network RPC: ${rpc}`)
console.log(`Spark App URL: ${buildAppUrl({ rpc, originChainId })}`)
return {
spellName,
originChainId,
forkRpc: rpc.publicRpcUrl,
forkChainId,
appUrl: buildAppUrl({ rpc: rpc.publicRpcUrl, originChainId }),
}
}

const arg1 = process.argv[2]

await main(arg1)
2 changes: 1 addition & 1 deletion src/utils/forge.ts → src/periphery/forge/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $ } from 'bun'
import { $ } from 'dax-sh'
import { Address } from 'viem'

export async function deployContract(contractName: string, rpc: string, from: Address): Promise<Address> {
Expand Down
9 changes: 7 additions & 2 deletions src/periphery/tenderly/TenderlyVnetClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export interface CreateTenderlyForkArgs {
}

export interface CreateTenderlyVnetResult {
rpcUrl: string
publicRpcUrl: string
adminRpcUrl: string
}

export class TenderlyVnetClient {
Expand Down Expand Up @@ -55,6 +56,10 @@ export class TenderlyVnetClient {
)

const data: any = await response.json()
return { rpcUrl: data.rpcs[0].url }

const adminRpc = data.rpcs.find((rpc: any) => rpc.name === 'Admin RPC')
const publicRpc = data.rpcs.find((rpc: any) => rpc.name === 'Public RPC')

return { publicRpcUrl: publicRpc.url, adminRpcUrl: adminRpc.url }
}
}
6 changes: 3 additions & 3 deletions src/periphery/tenderly/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { TenderlyVnetClient } from './TenderlyVnetClient'
import { CreateTenderlyVnetResult, TenderlyVnetClient } from './TenderlyVnetClient'

export async function createTenderlyVNet(opts: {
apiKey: string
account: string
project: string
originChainId: number
forkChainId: number
}): Promise<string> {
}): Promise<CreateTenderlyVnetResult> {
const client = new TenderlyVnetClient(opts)
const vnet = await client.create({
forkChainId: opts.forkChainId,
originChainId: opts.originChainId,
name: `spark-spell-${Date.now()}`,
})

return vnet.rpcUrl
return vnet
}

export function getRandomChainId(): number {
Expand Down
8 changes: 4 additions & 4 deletions src/executeSpell.test.ts → src/spells/executeSpell.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, expect, test } from 'bun:test'
import type { NetworkConfig } from './config'
import { NetworkConfig } from '../config'
import { getMockEthereumClient } from '../test/MockEthereumClient'
import { randomAddress } from '../test/addressUtils'
import { asciiToHex, hexStringToHex } from '../test/hexUtils'
import { executeSpell } from './executeSpell'
import { getMockEthereumClient } from './test/MockEthereumClient'
import { randomAddress } from './test/addressUtils'
import { asciiToHex, hexStringToHex } from './test/hexUtils'

describe(executeSpell.name, () => {
test('replaces the code of the executor with a code of a spell', async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/executeSpell.ts → src/spells/executeSpell.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'node:assert'
import { type Address, encodeFunctionData } from 'viem'
import type { NetworkConfig } from './config'
import type { IEthereumClient } from './periphery/ethereum'
import { NetworkConfig } from '../config'
import { IEthereumClient } from '../periphery/ethereum'

interface ExecuteSpellArgs {
spellAddress: Address
Expand Down
10 changes: 10 additions & 0 deletions src/spells/findPendingSpells.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, test } from 'bun:test'
import { getFilenameWithoutExtension } from './findPendingSpells'

describe(getFilenameWithoutExtension.name, () => {
test('gets filename without extension from a full path', () => {
const fullPath = '/path/to/file.sol'
const result = getFilenameWithoutExtension(fullPath)
expect(result).toBe('file')
})
})
22 changes: 22 additions & 0 deletions src/spells/findPendingSpells.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as fs from 'node:fs'
import * as path from 'node:path'
import * as glob from 'glob'

export function findPendingSpells(projectRootPath: string): string[] {
const proposalsDirPath = path.join(projectRootPath, 'src/proposals')

if (!fs.existsSync(proposalsDirPath)) {
throw new Error(`Directory not found: ${proposalsDirPath}`)
}

const spellsAndTests = glob.globSync('**/*.sol', { cwd: proposalsDirPath, absolute: true })

const spellPaths = spellsAndTests.filter((path) => !path.includes('.t.sol'))

return spellPaths.map(getFilenameWithoutExtension)
}

export function getFilenameWithoutExtension(fullPath: string): string {
const parsedPath = path.parse(fullPath)
return parsedPath.name
}

0 comments on commit 898ba65

Please sign in to comment.