-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #55 from sergeyk-symbiotic/main
Add metadata validation and extraction workflows
- Loading branch information
Showing
9 changed files
with
459 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
name: Get full info | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
generate: | ||
runs-on: self-hosted | ||
|
||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
|
||
- name: Set up Node.js | ||
uses: actions/setup-node@v4 | ||
with: | ||
node-version: '22' | ||
|
||
- name: Install dependencies | ||
run: npm install tsx | ||
|
||
- name: Generate | ||
run: npx tsx .github/workflows/scripts/extract-metadata.ts | ||
|
||
- name: Release latest full info | ||
uses: softprops/action-gh-release@v1 | ||
with: | ||
name: latest | ||
tag_name: latest | ||
files: full-info.json | ||
fail_on_unmatched_files: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import fs from 'fs/promises'; | ||
import path from 'path'; | ||
import { pathToFileURL } from 'url'; | ||
|
||
type Info = Partial<{ | ||
name: string; | ||
description: string; | ||
tags: string[]; | ||
cmcId: string; | ||
links: [ | ||
{ | ||
type: string; | ||
name: string; | ||
url: string; | ||
}, | ||
{ | ||
type: string; | ||
name: string; | ||
url: string; | ||
}, | ||
{ | ||
type: string; | ||
name: string; | ||
url: string; | ||
} | ||
]; | ||
}>; | ||
|
||
type Entity = { | ||
info: Info; | ||
logo: string; | ||
}; | ||
|
||
enum DIRECTORIES { | ||
VAULTS = 'vaults', | ||
TOKENS = 'tokens', | ||
NETWORKS = 'networks', | ||
OPERATORS = 'operators', | ||
POINTS = 'points', | ||
} | ||
|
||
type Template = Record<DIRECTORIES, Record<string, Entity>>; | ||
|
||
async function grabEntitiesInfo(globalDirs: DIRECTORIES[]) { | ||
const result = Object.values(DIRECTORIES).reduce<Template>((acc, curr) => { | ||
acc[curr] = {}; | ||
return acc; | ||
}, {} as Template); | ||
|
||
for (const dir of globalDirs) { | ||
try { | ||
const subdirs = await fs.readdir(dir); | ||
for (const subdir of subdirs) { | ||
const entityPath = path.join(dir, subdir); | ||
try { | ||
const infoPath = path.join(entityPath, 'info.json'); | ||
const infoUrl = pathToFileURL(infoPath).href; | ||
|
||
const module = await import(infoUrl); | ||
const info: Info = module.default; | ||
result[dir as DIRECTORIES][subdir] = { | ||
info, | ||
logo: | ||
'https://raw.githubusercontent.com/symbioticfi/metadata-holesky/main/' + | ||
entityPath + | ||
'/logo.png', | ||
}; | ||
} catch (error) { | ||
console.error('Error processing entity in ' + entityPath, error); | ||
} | ||
} | ||
} catch (error) { | ||
console.error('Error reading directory ' + dir, error); | ||
} | ||
} | ||
|
||
const filePath = path.join(process.cwd(), 'full-info.json'); | ||
await fs.writeFile(filePath, JSON.stringify(result, null, '\t'), 'utf8'); | ||
} | ||
|
||
grabEntitiesInfo(Object.values(DIRECTORIES)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import * as core from '@actions/core'; | ||
import * as github from '@actions/github'; | ||
|
||
const token = process.env.GITHUB_TOKEN; | ||
|
||
if (!token) { | ||
core.setFailed('GITHUB_TOKEN env variable is required'); | ||
process.exit(1); | ||
} | ||
|
||
const octokit = github.getOctokit(token); | ||
|
||
export type ReviewComment = { | ||
path: string; | ||
body: string; | ||
line?: number; | ||
position?: number; | ||
} | ||
|
||
export type Review = { | ||
body?: string; | ||
comments?: ReviewComment[]; | ||
} | ||
|
||
export const addComment = async (body: string) => { | ||
const { owner, repo, number } = github.context.issue; | ||
|
||
await octokit.rest.issues.createComment({ owner, repo, issue_number: number, body }); | ||
}; | ||
|
||
export const addReview = async (review: Review) => { | ||
const { owner, repo, number } = github.context.issue; | ||
|
||
await octokit.rest.pulls.createReview({ | ||
owner, | ||
repo, | ||
pull_number: number, | ||
event: 'COMMENT', | ||
...review, | ||
}); | ||
}; | ||
|
||
export const run = async (command: () => Promise<void>) => { | ||
try { | ||
await command(); | ||
} catch (error) { | ||
core.setFailed(error.message); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
const contributionGuidelines = `Please, follow the [contribution guidelines](https://github.com/symbioticfi/metadata-holesky/blob/main/README.md).`; | ||
|
||
type JSONSchemaError = { | ||
line: number; | ||
message: string; | ||
}; | ||
|
||
export const notAllowedChanges = (files: string[]) => | ||
`We detected changes in the pull request that are not allowed. ${contributionGuidelines} | ||
**Not allowed files:** | ||
${files.map((file) => `- ${file}`).join('\n')} | ||
`; | ||
|
||
export const onlyOneEntityPerPr = (dirs: string[]) => | ||
`It is not allowed to change more than one entity in a single pull request. ${contributionGuidelines} | ||
**Entities:** | ||
${dirs.map((file) => `- ${file}`).join('\n')} | ||
`; | ||
|
||
export const noInfoJson = (entityDir: string, files: string[]) => | ||
`The entity folder should have \`info.json\` file. ${contributionGuidelines}`; | ||
|
||
export const invalidInfoJson = (path: string, erros: JSONSchemaError[]) => | ||
`The \`info.json\` file is invalid. ${contributionGuidelines}`; | ||
|
||
export const invalidLogo = (path: string, errors: string[]) => | ||
`The logo image is invalid. ${contributionGuidelines} | ||
**Unmet requirements:** | ||
${errors.map((error) => `- ${error}`).join('\n')} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
{ | ||
"type": "object", | ||
"properties": { | ||
"name": { | ||
"type": "string" | ||
}, | ||
|
||
"tags": { | ||
"type": "array", | ||
"items": { | ||
"type": "string" | ||
} | ||
}, | ||
|
||
"links": { | ||
"type": "array", | ||
"items": { | ||
"type": "object", | ||
"properties": { | ||
"type": { | ||
"type": "string", | ||
"enum": ["website", "explorer", "docs", "example"] | ||
}, | ||
|
||
"name": { | ||
"type": "string" | ||
}, | ||
|
||
"url": { | ||
"type": "string", | ||
"format": "uri" | ||
} | ||
}, | ||
|
||
"required": ["type", "name", "url"] | ||
} | ||
}, | ||
|
||
"cmcId": { | ||
"type": "string" | ||
}, | ||
|
||
"permitName": { | ||
"type": "string" | ||
}, | ||
|
||
"permitVersion": { | ||
"type": "string" | ||
} | ||
}, | ||
|
||
"required": ["name", "tags", "links"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import path from 'path'; | ||
import fs from 'fs'; | ||
import * as core from '@actions/core'; | ||
import * as github from './github'; | ||
import * as messages from './messages'; | ||
|
||
const addressRegex = /^0x[a-fA-F0-9]{40}$/; | ||
const allowedTypes = ['vaults', 'operators', 'networks', 'tokens']; | ||
const allowedFiles = ['info.json', 'logo.png']; | ||
const changedFiles = process.argv.slice(2); | ||
|
||
github.run(async () => { | ||
const notAllowed = new Set<string>(); | ||
const entityDirs = new Set<string>(); | ||
|
||
for (const filePath of changedFiles) { | ||
const dir = path.dirname(filePath); | ||
const [type, address, fileName] = filePath.split(path.sep); | ||
const isValid = | ||
allowedTypes.includes(type) && addressRegex.test(address) && allowedFiles.includes(fileName); | ||
|
||
if (isValid) { | ||
entityDirs.add(dir); | ||
} else { | ||
notAllowed.add(filePath); | ||
} | ||
} | ||
|
||
/** | ||
* Validate that there are only allowed changes | ||
*/ | ||
if (notAllowed.size) { | ||
await github.addComment(messages.notAllowedChanges([...notAllowed])); | ||
|
||
throw new Error( | ||
`The pull request includes changes outside the allowed directories:\n ${[...notAllowed].join( | ||
', ' | ||
)}` | ||
); | ||
} | ||
|
||
/** | ||
* Validate that only one entity is changed per pull request | ||
*/ | ||
if (entityDirs.size > 1) { | ||
await github.addComment(messages.onlyOneEntityPerPr([...entityDirs])); | ||
|
||
throw new Error('Several entities are changed in one pull request'); | ||
} | ||
|
||
const [entityDir] = entityDirs; | ||
const existingFiles = await fs.promises.readdir(entityDir); | ||
|
||
const [metadataPath, logoPath] = allowedFiles.map((name) => { | ||
return existingFiles.includes(name) ? path.join(entityDir, name) : undefined; | ||
}); | ||
|
||
const [isMetadataChanged, isLogoChanged] = allowedFiles.map((name) => { | ||
return changedFiles.some((file) => path.basename(file) === name); | ||
}); | ||
|
||
/** | ||
* Validate that metadata present in the entity folder. | ||
*/ | ||
if (!metadataPath) { | ||
await github.addComment(messages.noInfoJson(entityDir, existingFiles)); | ||
|
||
throw new Error('`info.json` is not found in the entity folder'); | ||
} | ||
|
||
/** | ||
* Send metadata to the next validation step only if the file was changed and exists. | ||
*/ | ||
if (isMetadataChanged && metadataPath) { | ||
core.setOutput('metadata', metadataPath); | ||
} | ||
|
||
/** | ||
* Send logo to the next validation step only if the file was changed and exists. | ||
*/ | ||
if (isLogoChanged && logoPath) { | ||
core.setOutput('logo', logoPath); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import sharp from 'sharp'; | ||
import * as fs from 'fs/promises'; | ||
|
||
import * as github from './github'; | ||
import * as messages from './messages'; | ||
|
||
const [logoPath] = process.argv.slice(2); | ||
|
||
github.run(async () => { | ||
const image = sharp(logoPath); | ||
|
||
const errors: string[] = []; | ||
const { size } = await fs.stat(logoPath); | ||
const { hasAlpha, width = 0, height = 0, format } = await image.metadata(); | ||
|
||
if (format !== 'png') { | ||
errors.push('The image format should be PNG'); | ||
} | ||
|
||
if (size > 1024 * 1024) { | ||
errors.push('The image is too large. The maximum size is 1MB'); | ||
} | ||
|
||
if (!hasAlpha) { | ||
errors.push('The image background should be transparent'); | ||
} | ||
|
||
if (width != 256 || height != 256) { | ||
errors.push('The image size must be 256x256 pixels'); | ||
} | ||
|
||
if (errors.length) { | ||
await github.addComment(messages.invalidLogo(logoPath, errors)); | ||
|
||
throw new Error('The logo is invalid'); | ||
} | ||
}); |
Oops, something went wrong.