From 149e51c5675f451c44beb1d615e29f805c4c3e3f Mon Sep 17 00:00:00 2001 From: "Carina.Akaia.near" Date: Fri, 13 Oct 2023 22:02:50 +0400 Subject: [PATCH 1/3] feat: Enable SimpleMDE on testnet (#300) --- src/gigs-board/components/molecule/markdown-editor.jsx | 6 +++++- src/gigs-board/pages/Teams.jsx | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gigs-board/components/molecule/markdown-editor.jsx b/src/gigs-board/components/molecule/markdown-editor.jsx index 6c1bfb617..c1bd95e40 100644 --- a/src/gigs-board/components/molecule/markdown-editor.jsx +++ b/src/gigs-board/components/molecule/markdown-editor.jsx @@ -1,7 +1,11 @@ const MarkdownEditor = ({ data, onChange }) => { return ( Date: Tue, 17 Oct 2023 12:57:53 +0800 Subject: [PATCH 2/3] Implement server side search (#298) * Implement server side search * case insensitive search on content * add search test --- playwright-tests/search.spec.js | 20 + src/gigs-board/entity/post/List.jsx | 115 +-- src/gigs-board/feature/post-search/panel.jsx | 776 +++---------------- 3 files changed, 115 insertions(+), 796 deletions(-) create mode 100644 playwright-tests/search.spec.js diff --git a/playwright-tests/search.spec.js b/playwright-tests/search.spec.js new file mode 100644 index 000000000..76cb12936 --- /dev/null +++ b/playwright-tests/search.spec.js @@ -0,0 +1,20 @@ +import { test } from "@playwright/test"; + +test("should show post history for posts in the feed", async ({ page }) => { + await page.goto("/devgovgigs.near/widget/gigs-board.pages.Feed"); + + // Fill the search by content by to + const searchInputSelector = 'input.form-control[type="search"]'; + let searchInput = await page.waitForSelector(searchInputSelector, { + state: "visible", + }); + await searchInput.fill("zero knowledge"); + + const searchButton = await page.getByRole("button", { name: "Search" }); + await searchButton.click(); + + await page.waitForSelector('span:has-text("zero knowledge")', { + state: "visible", + timeout: 10000, + }); +}); diff --git a/src/gigs-board/entity/post/List.jsx b/src/gigs-board/entity/post/List.jsx index 140db7f9d..88e166892 100644 --- a/src/gigs-board/entity/post/List.jsx +++ b/src/gigs-board/entity/post/List.jsx @@ -126,9 +126,9 @@ function defaultRenderItem(postId, additionalProps) { const renderItem = props.renderItem ?? defaultRenderItem; const cachedRenderItem = (item, i) => { - if (props.searchResult && props.searchResult.keywords[item]) { + if (props.searchResult && props.searchResult.keywords) { return renderItem(item, { - searchKeywords: props.searchResult.keywords[item], + searchKeywords: props.searchResult.keywords, }); } @@ -144,61 +144,6 @@ const cachedRenderItem = (item, i) => { const initialRenderLimit = props.initialRenderLimit ?? 3; const addDisplayCount = props.nextLimit ?? initialRenderLimit; -function getPostsByLabel() { - let postIds = Near.view( - nearDevGovGigsContractAccountId, - "get_posts_by_label", - { - label: props.tag, - } - ); - if (postIds) { - postIds.reverse(); - } - return postIds; -} - -function getPostsByAuthor() { - let postIds = Near.view( - nearDevGovGigsContractAccountId, - "get_posts_by_author", - { - author: props.author, - } - ); - if (postIds) { - postIds.reverse(); - } - return postIds; -} - -function intersectPostsWithLabel(postIds) { - if (props.tag) { - let postIdLabels = getPostsByLabel(); - if (postIdLabels === null) { - // wait until postIdLabels are loaded - return null; - } - postIdLabels = new Set(postIdLabels); - return postIds.filter((id) => postIdLabels.has(id)); - } - return postIds; -} - -function intersectPostsWithAuthor(postIds) { - if (props.author) { - let postIdsByAuthor = getPostsByAuthor(); - if (postIdsByAuthor == null) { - // wait until postIdsByAuthor are loaded - return null; - } else { - postIdsByAuthor = new Set(postIdsByAuthor); - return postIds.filter((id) => postIdsByAuthor.has(id)); - } - } - return postIds; -} - const ONE_DAY = 60 * 60 * 24 * 1000; const ONE_WEEK = 60 * 60 * 24 * 1000 * 7; const ONE_MONTH = 60 * 60 * 24 * 1000 * 30; @@ -225,61 +170,9 @@ const getPeriodText = (period) => { return text; }; -const findHottestsPosts = (postIds, period) => { - let allPosts; - if (!state.allPosts) { - allPosts = Near.view("devgovgigs.near", "get_posts"); - if (!allPosts) { - return []; - } - State.update({ allPosts }); - } else { - allPosts = state.allPosts; - } - let postIdsSet = new Set(postIds); - let posts = allPosts.filter((post) => postIdsSet.has(post.id)); - - let periodTime = ONE_DAY; - if (period === "week") { - periodTime = ONE_WEEK; - } - if (period === "month") { - periodTime = ONE_MONTH; - } - const periodLimitedPosts = posts.filter((post) => { - const timestamp = post.snapshot.timestamp / 1000000; - return Date.now() - timestamp < periodTime; - }); - const modifiedPosts = periodLimitedPosts.map((post) => { - const comments = - Near.view("devgovgigs.near", "get_children_ids", { - post_id: post.id, - }) || []; - post = { ...post, comments }; - return { - ...post, - postScore: getHotnessScore(post), - }; - }); - modifiedPosts.sort((a, b) => b.postScore - a.postScore); - return modifiedPosts.map((post) => post.id); -}; - let postIds; if (props.searchResult) { postIds = props.searchResult.postIds; - postIds = intersectPostsWithLabel(postIds); - postIds = intersectPostsWithAuthor(postIds); -} else if (props.tag) { - postIds = getPostsByLabel(); - postIds = intersectPostsWithAuthor(postIds); -} else if (props.author) { - postIds = getPostsByAuthor(); -} else if (props.recency == "all") { - postIds = Near.view(nearDevGovGigsContractAccountId, "get_all_post_ids"); - if (postIds) { - postIds.reverse(); - } } else { postIds = Near.view(nearDevGovGigsContractAccountId, "get_children_ids"); if (postIds) { @@ -287,10 +180,6 @@ if (props.searchResult) { } } -if (props.recency == "hot") { - postIds = findHottestsPosts(postIds, state.period); -} - const loader = (
{ - if (synonyms.hasOwnProperty(word.toLowerCase())) { - return synonyms[word]; - } - return word; -}; -////////////////////////////////////////////////////////////////////// -///STEMMING/////////////////////////////////////////////////////////// -const step2list = { - ational: "ate", - tional: "tion", - enci: "ence", - anci: "ance", - izer: "ize", - bli: "ble", - alli: "al", - entli: "ent", - eli: "e", - ousli: "ous", - ization: "ize", - ation: "ate", - ator: "ate", - alism: "al", - iveness: "ive", - fulness: "ful", - ousness: "ous", - aliti: "al", - iviti: "ive", - biliti: "ble", - logi: "log", -}; - -/** @type {Record} */ -const step3list = { - icate: "ic", - ative: "", - alize: "al", - iciti: "ic", - ical: "ic", - ful: "", - ness: "", -}; - -const gt0 = /^([^aeiou][^aeiouy]*)?([aeiouy][aeiou]*)([^aeiou][^aeiouy]*)/; -const eq1 = - /^([^aeiou][^aeiouy]*)?([aeiouy][aeiou]*)([^aeiou][^aeiouy]*)([aeiouy][aeiou]*)?$/; -const gt1 = - /^([^aeiou][^aeiouy]*)?(([aeiouy][aeiou]*)([^aeiou][^aeiouy]*)){2,}/; -const vowelInStem = /^([^aeiou][^aeiouy]*)?[aeiouy]/; -const consonantLike = /^([^aeiou][^aeiouy]*)[aeiouy][^aeiouwxy]$/; - -// Exception expressions. -const sfxLl = /ll$/; -const sfxE = /^(.+?)e$/; -const sfxY = /^(.+?)y$/; -const sfxIon = /^(.+?(s|t))(ion)$/; -const sfxEdOrIng = /^(.+?)(ed|ing)$/; -const sfxAtOrBlOrIz = /(at|bl|iz)$/; -const sfxEED = /^(.+?)eed$/; -const sfxS = /^.+?[^s]s$/; -const sfxSsesOrIes = /^.+?(ss|i)es$/; -const sfxMultiConsonantLike = /([^aeiouylsz])\1$/; -const step2 = - /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; -const step3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; -const step4 = - /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - -/** - * Get the stem from a given value. - * - * @param {string} value - * Value to stem. - * @returns {string} - * Stem for `value` - */ -// eslint-disable-next-line complexity -function stemmer(value) { - let result = value.toLowerCase(); - - // Exit early. - if (result.length < 3) { - return result; - } - - /** @type {boolean} */ - let firstCharacterWasLowerCaseY = false; - - // Detect initial `y`, make sure it never matches. - if ( - result.codePointAt(0) === 121 // Lowercase Y - ) { - firstCharacterWasLowerCaseY = true; - result = "Y" + result.slice(1); - } - - // Step 1a. - if (sfxSsesOrIes.test(result)) { - // Remove last two characters. - result = result.slice(0, -2); - } else if (sfxS.test(result)) { - // Remove last character. - result = result.slice(0, -1); - } - - /** @type {RegExpMatchArray|null} */ - let match; - - // Step 1b. - if ((match = sfxEED.exec(result))) { - if (gt0.test(match[1])) { - // Remove last character. - result = result.slice(0, -1); - } - } else if ((match = sfxEdOrIng.exec(result)) && vowelInStem.test(match[1])) { - result = match[1]; - - if (sfxAtOrBlOrIz.test(result)) { - // Append `e`. - result += "e"; - } else if (sfxMultiConsonantLike.test(result)) { - // Remove last character. - result = result.slice(0, -1); - } else if (consonantLike.test(result)) { - // Append `e`. - result += "e"; - } - } - - // Step 1c. - if ((match = sfxY.exec(result)) && vowelInStem.test(match[1])) { - // Remove suffixing `y` and append `i`. - result = match[1] + "i"; - } - - // Step 2. - if ((match = step2.exec(result)) && gt0.test(match[1])) { - result = match[1] + step2list[match[2]]; - } - - // Step 3. - if ((match = step3.exec(result)) && gt0.test(match[1])) { - result = match[1] + step3list[match[2]]; - } - - // Step 4. - if ((match = step4.exec(result))) { - if (gt1.test(match[1])) { - result = match[1]; - } - } else if ((match = sfxIon.exec(result)) && gt1.test(match[1])) { - result = match[1]; - } - - // Step 5. - if ( - (match = sfxE.exec(result)) && - (gt1.test(match[1]) || - (eq1.test(match[1]) && !consonantLike.test(match[1]))) - ) { - result = match[1]; - } - - if (sfxLl.test(result) && gt1.test(result)) { - result = result.slice(0, -1); - } - - // Turn initial `Y` back to `y`. - if (firstCharacterWasLowerCaseY) { - result = "y" + result.slice(1); - } - - return result; -} - -////////////////////////////////////////////////////////////////////// -///SPELLCHECK///////////////////////////////////////////////////////// -function levenshteinDistance(s, t, threshold) { - const BIG_NUMBER = 10000; - if (s == null || t == null) { - return BIG_NUMBER; - } - if (threshold < 0) { - return BIG_NUMBER; - } - let n = s.length; - let m = t.length; - if (Math.abs(n - m) >= threshold) { - return BIG_NUMBER; - } - - // if one string is empty, the edit distance is necessarily the length of the other - if (n == 0) { - return m <= threshold ? m : BIG_NUMBER; - } else if (m == 0) { - return n <= threshold ? n : BIG_NUMBER; - } - - if (n > m) { - // swap the two strings to consume less memory - let temp = s; - s = t; - t = temp; - let tempSize = n; - n = m; - m = tempSize; - } - - let p = Array.from({ length: n + 1 }, () => 0); // 'previous' cost array, horizontally - let d = Array.from({ length: n + 1 }, () => 0); // cost array, horizontally - let _d; // placeholder to assist in swapping p and d - - // fill in starting table values - const boundary = Math.min(n, threshold) + 1; - for (let i = 0; i < boundary; i++) { - p[i] = i; - } - // these fills ensure that the value above the rightmost entry of our - // stripe will be ignored in following loop iterations - for (let i = boundary; i < p.length; i++) { - p[i] = BIG_NUMBER; - } - for (let i = 0; i < d.length; i++) { - d[i] = BIG_NUMBER; - } - - // iterates through t - for (let j = 1; j <= m; j++) { - const t_j = t.charAt(j - 1); // jth character of t - d[0] = j; - - // compute stripe indices, constrain to array size - const min = Math.max(1, j - threshold); - const max = j > BIG_NUMBER - threshold ? n : Math.min(n, j + threshold); - - // the stripe may lead off of the table if s and t are of different sizes - if (min > max) { - return BIG_NUMBER; - } - - // ignore entry left of leftmost - if (min > 1) { - d[min - 1] = BIG_NUMBER; - } - - // iterates through [min, max] in s - for (let i = min; i <= max; i++) { - if (s.charAt(i - 1) == t_j) { - // diagonally left and up - d[i] = p[i - 1]; - } else { - // 1 + minimum of cell to the left, to the top, diagonally left and up - d[i] = 1 + Math.min(Math.min(d[i - 1], p[i]), p[i - 1]); - } +const QUERYAPI_ENDPOINT = `https://near-queryapi.api.pagoda.co/v1/graphql/`; + +const queryName = + props.queryName ?? `bo_near_devhub_v17_posts_with_latest_snapshot`; + +const query = `query DevhubPostsQuery($limit: Int = 100, $offset: Int = 0, $where: ${queryName}_bool_exp = {}) { + ${queryName}( + limit: $limit + offset: $offset + order_by: {block_height: desc} + where: $where + ) { + post_id } - - // copy current distance counts to 'previous row' distance counts - _d = p; - p = d; - d = _d; } - // we don't need to check for threshold here because we did it inside the loop - return p[n] <= threshold ? p[n] : BIG_NUMBER; +`; + +function fetchGraphQL(operationsDoc, operationName, variables) { + return asyncFetch(QUERYAPI_ENDPOINT, { + method: "POST", + headers: { "x-hasura-role": `bo_near` }, + body: JSON.stringify({ + query: operationsDoc, + variables: variables, + operationName: operationName, + }), + }); } -const spellcheckQueryProcessing = (query, dictionary) => { - // Split text document into words - const words = stemAndFilterQuery(query); - const dictionaryArray = Object.keys(dictionary); - // Iterate over each word in the text - for (let i = 0; i < words.length; i++) { - let word = words[i].toLowerCase().replace(/[^a-z0-9]/g, ""); - - // If the word is not in the dictionary, find the closest match - if (!dictionary.hasOwnProperty(word)) { - let closestMatch = undefined; - let closestDistance = word.length; - let allowedDistance = Math.min(word.length - 1, 3); - // Iterate over each word in the dictionary - if (word.length > 1) { - for (let j = 0; j < dictionaryArray.length; j++) { - let dictWord = dictionaryArray[j]; - let distance = levenshteinDistance(word, dictWord, allowedDistance); - - // If the distance is less than the closest distance, update the closest match - if (distance <= allowedDistance && distance < closestDistance) { - closestMatch = dictWord; - closestDistance = distance; - } - } - } - // Replace the misspelled word with the closest match - words[i] = closestMatch; - } - } - return words.filter((word) => !!word); -}; - -////////////////////////////////////////////////////////////////////// -///INDEXER&SEARCH///////////////////////////////////////////////////// -const fillDictionaryWith = (dict, text, id) => { - let word = ""; - for (let i = 0; i < text.length; i++) { - const char = text.charAt(i); - const nextChar = text.charAt(i + 1); - if (/\w/.test(char) || (char === "." && /\w/.test(nextChar))) { - word += char.toLowerCase(); - } else if (word.length > 0) { - const processedWord = applySynonym(stemmer(word)); - if (processedWord.length > 1 && !isStopWord(processedWord)) { - const oldValue = dict[processedWord] || []; - dict[processedWord] = [...oldValue, id]; +function search() { + State.update({ loading: true }); + let where = {}; + if (props.authorQuery && props.authorQuery.author) { + where = { author_id: { _eq: props.authorQuery.author }, ...where }; + } + if (state.term) { + where = { description: { _ilike: `%${state.term}%` }, ...where }; + } + if (props.tagQuery && props.tagQuery.tag) { + where = { labels: { _contains: props.tagQuery.tag }, ...where }; + } + console.log("searching for", where); + fetchGraphQL(query, "DevhubPostsQuery", { + limit: 100, + offset: 0, + where, + }).then((result) => { + if (result.status === 200) { + console.log("search success"); + if (result.body.data) { + const data = result.body.data[queryName]; + State.update({ + searchResult: { + postIds: data.map((p) => p.post_id), + keywords: state.term ? [state.term] : undefined, + }, + }); + console.log("found:"); + console.log(data); } - word = ""; - } - } - const processedWord = applySynonym(stemmer(word)); - if (processedWord.length > 1 && !isStopWord(processedWord)) { - const oldValue = dict[stemmer(processedWord)] || []; - dict[stemmer(processedWord)] = [...oldValue, id]; - } - return dict; -}; - -const buildIndex = (posts) => { - let index = {}; - - posts.forEach((post) => { - const title = post.snapshot.name; - const labels = post.snapshot.labels.join(" "); - const text = post.snapshot.description; - const postType = post.snapshot.post_type; - const authorId = post.author_id; - const postText = `${authorId} ${postType} ${title} ${labels} ${text}`; - index = fillDictionaryWith(index, postText, post.id); - }); - return index; -}; - -const stemAndFilterQuery = (query) => { - return Object.keys(fillDictionaryWith({}, query)); -}; - -const sortSearchResult = (searchResult) => { - // create a map to count the frequency of each element - const freq = new Map(); - for (const num of searchResult) { - freq.set(num, (freq.get(num) || 0) + 1); - } - - // define a custom comparison function to sort the array - function compare(a, b) { - // compare the frequency of the two elements - const freqDiff = freq.get(b) - freq.get(a); - if (freqDiff !== 0) { - return freqDiff; // if they have different frequency, sort by frequency } else { - return 0; // if they have the same frequency, leave as it is. Will be sorted by search term, by date - } - } - - // sort the array using the custom comparison function - searchResult.sort(compare); - return searchResult.filter( - (elem, index) => searchResult.indexOf(elem) === index - ); -}; - -const search = (processedQueryArray, index) => { - return sortSearchResult( - processedQueryArray.flatMap((queryWord) => { - const termSearchRes = index[queryWord].reverse(); - const termSortedSearchRes = sortSearchResult(termSearchRes); - return termSortedSearchRes; - }) - ); -}; - -////////////////////////////////////////////////////////////////////// -///UI&UX////////////////////////////////////////////////////////////// -//Run search and spelling computation every time the search bar modified -//but no more frequent than 1 time per 1.5 seconds -const amountOfResultsToShowFirst = 5; - -const buildPostsIndex = () => { - return Near.asyncView("devgovgigs.near", "get_posts").then((posts) => { - const index = buildIndex(posts); - const data = posts.reduce((acc, post) => { - acc[post.id] = post; - return acc; - }, {}); - return { index, data }; - }); -}; - -const getProcessedPostsCached = () => { - return useCache(() => buildPostsIndex(), "processedPostsCached"); -}; - -if (!state.interval) { - let termStorage = ""; - Storage.privateSet("term", ""); - setInterval(() => { - const currentInput = Storage.privateGet("term"); - if (currentInput !== termStorage) { - termStorage = currentInput; - computeResults(termStorage); + console.error("error:", result.body); } - }, 1500); - State.update({ - interval: true, + State.update({ loading: false }); }); } -const computeResults = (term) => { - const start = new Date().getTime(); - const processedPostsCached = useCache( - () => - buildPostsIndex().then((processedPosts) => { - // Run query first time posts retrieved - const query = term; - const processedQuery = spellcheckQueryProcessing( - query, - processedPosts.index - ); - const searchResult = search(processedQuery, processedPosts.index); - console.log(processedQuery); - console.log(searchResult); - State.update({ - searchResult, - shownSearchResults: searchResult.slice(0, amountOfResultsToShowFirst), - processedQuery, - loading: false, - }); - return processedPosts; - }), - "processedPostsCached" - ); - if (processedPostsCached) { - // Run query every other time after data retrieved and cached - const query = term; - const processedQuery = spellcheckQueryProcessing( - query, - processedPostsCached.index - ); - const searchResult = search(processedQuery, processedPostsCached.index); - console.log(processedQuery); - console.log(searchResult); - State.update({ - searchResult, - shownSearchResults: searchResult.slice(0, 10), - processedQuery, - loading: false, - }); - } - const end = new Date().getTime(); - console.log("search time: ", end - start); -}; - const updateInput = (term) => { - Storage.privateSet("term", term); State.update({ term, - loading: true, }); }; -const getSearchResultsKeywordsFor = (postId) => { - const index = getProcessedPostsCached().index; - return state.processedQuery.filter((queryWord) => { - return index[queryWord].includes(postId); - }); -}; - -const showMoreSearchResults = () => { - const shownSearchResults = state.shownSearchResults || []; - const newShownSearchResults = state.searchResult.slice( - 0, - shownSearchResults.length + amountOfResultsToShowFirst - ); - State.update({ shownSearchResults: newShownSearchResults }); +const buttonStyle = { + backgroundColor: "#0C7283", + color: "#f3f3f3", }; return ( <>
-
-
- {state.loading ? ( -
- updateInput(e.target.value)} - placeholder={props.placeholder ?? `Search Posts`} - /> -
- +
+ updateInput(e.target.value)} + placeholder={props.placeholder ?? `Search by content`} + /> +
+ + {state.searchResult ? ( + + ) : ( + "" + )}
{props.children}
- {state.processedQuery && - state.processedQuery.length > 0 && - state.term.toLowerCase().trim() !== state.processedQuery.join(" ") && ( -
- Looking for - {state.processedQuery.join(" ")}: -
- )} - {state.term && state.term.length > 1 && state.searchResult + {state.searchResult ? widget("entity.post.List", { - searchResult: { - postIds: state.searchResult, - keywords: Object.fromEntries( - state.searchResult.map((postId) => { - return [postId, getSearchResultsKeywordsFor(postId)]; - }) - ), - }, + searchResult: state.searchResult, recency: props.recency, - tag: props.tag, - author: props.author, }) : widget("entity.post.List", { recency: props.recency, - tag: props.tag, - author: props.author, transactionHashes: props.transactionHashes, })} From e0dc11dc1a32845ccee767d0b63706c308c062f5 Mon Sep 17 00:00:00 2001 From: Elliot Braem <16282460+elliotBraem@users.noreply.github.com> Date: Tue, 17 Oct 2023 20:25:13 -0400 Subject: [PATCH 3/3] feat: adds workflow to deploy to testnet (#305) * feat: adds workflow to deploy to testnet * fix: prettier * fix: build script * feature: devhub.testnet workflow * feat: creates dev script * feat: replaces release workflow * feat: adds weekly promotion of develop to main --- .github/workflows/deploy-dev-testnet.yml | 39 ++++++++++++ .github/workflows/deploy-prod-mainnet.yml | 40 ++++++++++++ .github/workflows/deploy-prod-testnet.yml | 39 ++++++++++++ .github/workflows/promote-develop-to-main.yml | 22 +++++++ .github/workflows/release.yml | 13 ---- .gitignore | 3 + CONTRIBUTING.md | 55 ++++++++++++++++- package.json | 2 +- replacements.dev.json | 7 +++ replacements.testnet.json | 7 +++ scripts/dev.sh | 61 +++++++++++++++++++ 11 files changed, 273 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/deploy-dev-testnet.yml create mode 100644 .github/workflows/deploy-prod-mainnet.yml create mode 100644 .github/workflows/deploy-prod-testnet.yml create mode 100644 .github/workflows/promote-develop-to-main.yml delete mode 100644 .github/workflows/release.yml create mode 100644 replacements.dev.json create mode 100644 replacements.testnet.json create mode 100644 scripts/dev.sh diff --git a/.github/workflows/deploy-dev-testnet.yml b/.github/workflows/deploy-dev-testnet.yml new file mode 100644 index 000000000..b137c8f87 --- /dev/null +++ b/.github/workflows/deploy-dev-testnet.yml @@ -0,0 +1,39 @@ +name: Deploy Widgets to Testnet Dev +on: + push: + branches: [develop] +jobs: + deploy-widgets: + runs-on: ubuntu-latest + name: Deploy widgets to devhub-dev.testnet + env: + NEAR_SOCIAL_ACCOUNT_ID: ${{ vars.NEAR_SOCIAL_TESTNET_DEV_ACCOUNT_ID }} + NEAR_SOCIAL_ACCOUNT_PUBLIC_KEY: ${{ vars.NEAR_SOCIAL_TESTNET_DEV_ACCOUNT_PUBLIC_KEY }} + NEAR_SOCIAL_ACCOUNT_PRIVATE_KEY: ${{ secrets.NEAR_SOCIAL_TESTNET_DEV_ACCOUNT_PRIVATE_KEY }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set replacements + id: set_replacements + run: | + echo "replacements=$(jq -r '[to_entries[] | .["find"] = "${" + .key + "}" | .["replace"] = .value | del(.key, .value)]' replacements.dev.json | tr -d "\n\r")" >> $GITHUB_OUTPUT + + - name: Replace placeholders + uses: flcdrg/replace-multiple-action@v1 + with: + files: '**/*.jsx' + find: '${{ steps.set_replacements.outputs.replacements }}' + prefix: '(^|.*)' + suffix: '($|.*)' + + - name: Install bos CLI + run: | + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/FroVolod/bos-cli-rs/releases/download/v0.3.5/bos-cli-v0.3.1-installer.sh | sh + + - name: Deploy widgets + run: | + which bos + echo $PATH + bos components deploy "$NEAR_SOCIAL_ACCOUNT_ID" sign-as "$NEAR_SOCIAL_ACCOUNT_ID" network-config testnet sign-with-plaintext-private-key --signer-public-key "$NEAR_SOCIAL_ACCOUNT_PUBLIC_KEY" --signer-private-key "$NEAR_SOCIAL_ACCOUNT_PRIVATE_KEY" send diff --git a/.github/workflows/deploy-prod-mainnet.yml b/.github/workflows/deploy-prod-mainnet.yml new file mode 100644 index 000000000..ac3631724 --- /dev/null +++ b/.github/workflows/deploy-prod-mainnet.yml @@ -0,0 +1,40 @@ +name: Deploy Widgets to Mainnet +on: + push: + branches: [main] +jobs: + deploy-widgets: + runs-on: ubuntu-latest + name: Deploy widgets to social.near (mainnet) + env: + NEAR_SOCIAL_DEPLOY_ID: ${{ vars.NEAR_SOCIAL_ACCOUNT_ID }} + NEAR_SOCIAL_ACCOUNT_ID: ${{ vars.NEAR_SOCIAL_ACCOUNT_ID }} + NEAR_SOCIAL_ACCOUNT_PUBLIC_KEY: ${{ vars.NEAR_SOCIAL_ACCOUNT_PUBLIC_KEY }} + NEAR_SOCIAL_ACCOUNT_PRIVATE_KEY: ${{ secrets.NEAR_SOCIAL_ACCOUNT_PRIVATE_KEY }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set replacements + id: set_replacements + run: | + echo "replacements=$(jq -r '[to_entries[] | .["find"] = "${" + .key + "}" | .["replace"] = .value | del(.key, .value)]' replacements.mainnet.json | tr -d "\n\r")" >> $GITHUB_OUTPUT + + - name: Replace placeholders + uses: flcdrg/replace-multiple-action@v1 + with: + files: '**/*.jsx' + find: '${{ steps.set_replacements.outputs.replacements }}' + prefix: '(^|.*)' + suffix: '($|.*)' + + - name: Install bos CLI + run: | + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/FroVolod/bos-cli-rs/releases/download/v0.3.2/bos-cli-installer.sh | sh + + - name: Deploy widgets + run: | + which bos + echo $PATH + bos components deploy "$NEAR_SOCIAL_DEPLOY_ID" sign-as "$NEAR_SOCIAL_ACCOUNT_ID" network-config mainnet sign-with-plaintext-private-key --signer-public-key "$NEAR_SOCIAL_ACCOUNT_PUBLIC_KEY" --signer-private-key "$NEAR_SOCIAL_ACCOUNT_PRIVATE_KEY" send diff --git a/.github/workflows/deploy-prod-testnet.yml b/.github/workflows/deploy-prod-testnet.yml new file mode 100644 index 000000000..2ae36191e --- /dev/null +++ b/.github/workflows/deploy-prod-testnet.yml @@ -0,0 +1,39 @@ +name: Deploy Widgets to Testnet +on: + push: + branches: [main] +jobs: + deploy-widgets: + runs-on: ubuntu-latest + name: Deploy widgets to devhub.testnet + env: + NEAR_SOCIAL_ACCOUNT_ID: ${{ vars.NEAR_SOCIAL_TESTNET_ACCOUNT_ID }} + NEAR_SOCIAL_ACCOUNT_PUBLIC_KEY: ${{ vars.NEAR_SOCIAL_TESTNET_ACCOUNT_PUBLIC_KEY }} + NEAR_SOCIAL_ACCOUNT_PRIVATE_KEY: ${{ secrets.NEAR_SOCIAL_TESTNET_ACCOUNT_PRIVATE_KEY }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set replacements + id: set_replacements + run: | + echo "replacements=$(jq -r '[to_entries[] | .["find"] = "${" + .key + "}" | .["replace"] = .value | del(.key, .value)]' replacements.testnet.json | tr -d "\n\r")" >> $GITHUB_OUTPUT + + - name: Replace placeholders + uses: flcdrg/replace-multiple-action@v1 + with: + files: '**/*.jsx' + find: '${{ steps.set_replacements.outputs.replacements }}' + prefix: '(^|.*)' + suffix: '($|.*)' + + - name: Install bos CLI + run: | + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/FroVolod/bos-cli-rs/releases/download/v0.3.5/bos-cli-installer.sh | sh + + - name: Deploy widgets + run: | + which bos + echo $PATH + bos components deploy "$NEAR_SOCIAL_ACCOUNT_ID" sign-as "$NEAR_SOCIAL_ACCOUNT_ID" network-config testnet sign-with-plaintext-private-key --signer-public-key "$NEAR_SOCIAL_ACCOUNT_PUBLIC_KEY" --signer-private-key "$NEAR_SOCIAL_ACCOUNT_PRIVATE_KEY" send \ No newline at end of file diff --git a/.github/workflows/promote-develop-to-main.yml b/.github/workflows/promote-develop-to-main.yml new file mode 100644 index 000000000..a2de05c5d --- /dev/null +++ b/.github/workflows/promote-develop-to-main.yml @@ -0,0 +1,22 @@ +name: weekly-production-release + +on: + schedule: + - cron: '0 0 * * THU' + +permissions: + pull-requests: write + +jobs: + pull-request: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/develop' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: pull-request + run: | + gh pr create --base main --head develop -f -a ailisp -a frol --title "weekly promotion of develop to main" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 7265acbcb..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Release -on: - push: - branches: [main] -jobs: - deploy-mainnet: - uses: FroVolod/bos-cli-rs/.github/workflows/deploy-mainnet.yml@master - with: - deploy-account-address: ${{ vars.NEAR_SOCIAL_ACCOUNT_ID }} - signer-account-address: ${{ vars.NEAR_SOCIAL_ACCOUNT_ID }} - signer-public-key: ${{ vars.NEAR_SOCIAL_ACCOUNT_PUBLIC_KEY }} - secrets: - SIGNER_PRIVATE_KEY: ${{ secrets.NEAR_SOCIAL_ACCOUNT_PRIVATE_KEY }} diff --git a/.gitignore b/.gitignore index 5aa32a21e..8d7872063 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules .vscode .DS_Store + +// replacements +replacements.*.json.tmp \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca4069d02..ea8b07eac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,10 +20,63 @@ You can also explore a list of [good first issues](https://github.com/near/devgi We use `npm` and scripts in `package.json` to automate common developer tasks, so you will need NodeJS and then install project dependencies as usual: -``` +```sh npm install ``` +### Development + +#### Run the dev script + +To initiate the development environment, you can use the following script. This script will first check if bos-loader is installed and, if not, install it. Then, it will serve the default development environment. + +```sh +npm run dev +``` + +#### Customizing Creator ID, Contract ID, or Network + +If you need to customize the account ID, override the contract ID, or specify a different network for your development environment, you can use the following flags: + +-a or --account: Specify the desired account ID. +-c or --contract: Override the default contract ID. +-n or --network: Set the network environment. + +Here's how you can use these flags: + +```sh +npm run dev -a your_account_id -c your_contract_id -n your_network +``` + +For example: + +```sh +npm run dev -a bob.near -c contract.bobs.near -n mainnet +``` + +By using these flags, you have the flexibility to customize the development environment according to your specific needs. + + +#### Developing across multiple environments + +When referencing a component or any parameter that depends on the network, please use the placeholders defined in replacements.*.json. There are three such files that correspond to different environments: + +`replacements.dev.json` - deploys the develop branch, to testnet @ test.beta.near.org + +`replacements.testnet.json` - deploys main branch, to testnet @ test.near.org + +`replacements.mainnet.json` - deploys main branch to mainnet @ near.org + +Placeholders should be encapsulated in the ${} expression. Here is an example of a placeholder usage: + +`` + +Placeholders are replaced with the target values specified in replacements.json when github actions deploys the components. + +Feel free to specify a new placeholder if needed. The placeholder should have a REPL prefix, for example: `REPL_PROJECT_NAME` + +A new placeholder should be defined for all three environments: dev-testnet, prod-testnet and prod-mainnet. + ### Deployment #### Deploy for Local Development and Testing diff --git a/package.json b/package.json index 4cb5bd68a..2c06a1be3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "fmt": "prettier --write '**/*.{js,jsx,ts,tsx,json}'", "fmt:check": "prettier --check '**/*.{js,jsx,ts,tsx,json}'", "build": "npm run fmt && node ./module/include.js", - "dev": "~/.cargo/bin/bos-loader devgovgigs.near --path src", + "dev": "sh ./scripts/dev.sh", "test": "npx playwright test" }, "repository": { diff --git a/replacements.dev.json b/replacements.dev.json new file mode 100644 index 000000000..e3000b7fb --- /dev/null +++ b/replacements.dev.json @@ -0,0 +1,7 @@ +{ + "REPL_DEVHUB": "devhub-dev.testnet", + "REPL_DEVHUB_CONTRACT": "thomaspreview.testnet", + "REPL_NEAR": "discom.testnet", + "REPL_MOB": "eugenethedream", + "REPL_EFIZ": "efiz.testnet" +} diff --git a/replacements.testnet.json b/replacements.testnet.json new file mode 100644 index 000000000..ec10dd732 --- /dev/null +++ b/replacements.testnet.json @@ -0,0 +1,7 @@ +{ + "REPL_DEVHUB": "devhub.testnet", + "REPL_DEVHUB_CONTRACT": "thomaspreview.testnet", + "REPL_NEAR": "discom.testnet", + "REPL_MOB": "eugenethedream", + "REPL_EFIZ": "efiz.testnet" +} diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100644 index 000000000..e60090dc8 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Check if bos-loader is installed +if ! command -v bos-loader &> /dev/null; then + # Install bos-loader + echo "bos-loader is not installed. Installing..." + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/mpeterdev/bos-loader/releases/download/v0.7.1/bos-loader-v0.7.1-installer.sh | sh +fi + +# Define default values +ACCOUNT_ID="devhub.testnet" +CONTRACT_ID="devhub.testnet" +NETWORK_ENV="testnet" +CREATOR_REPL="REPL_DEVHUB" +CONTRACT_REPL="REPL_DEVHUB_CONTRACT" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + -a|--account) + ACCOUNT_ID="$2" + shift + shift + ;; + -c|--contract) + CONTRACT_ID="$2" + shift + shift + ;; + -n|--network) + NETWORK_ENV="$2" + shift + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Convert NETWORK_ENV to lowercase +NETWORK_ENV=$(echo "$NETWORK_ENV" | tr '[:upper:]' '[:lower:]') + +echo "NETWORK_ENV: $NETWORK_ENV" + +# Update the value in replacements.json +REPLACEMENTS_JSON="replacements.$NETWORK_ENV.json" + +if [ -f "$REPLACEMENTS_JSON" ]; then + # Replace the value in the JSON file + + jq --arg ACCOUNT_ID "$ACCOUNT_ID" --arg CONTRACT_ID "$CONTRACT_ID" ".[\"$CREATOR_REPL\"] = \"$ACCOUNT_ID\" | .[\"$CONTRACT_REPL\"] = \"$CONTRACT_ID\"" "$REPLACEMENTS_JSON" > "$REPLACEMENTS_JSON.tmp" +else + echo "Error: $REPLACEMENTS_JSON file not found." + exit 1 +fi + +# Run bos-loader with updated replacements +bos-loader "$ACCOUNT_ID" --path src -r "$REPLACEMENTS_JSON"