From 2db080690fcea2ada04172332f265d9d1972faed Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 15 Feb 2026 09:39:43 -0800 Subject: [PATCH] Add shared actions and workflows --- actions/lint-test-build/action.yml | 56 +++++++++ actions/node-ci/action.yml | 75 ++++++++++++ actions/restore-assets/action.yml | 62 ++++++++++ actions/setup-rust/action.yml | 56 +++++++++ actions/sync-assets/action.yml | 84 +++++++++++++ workflows/project-board-automation.yml | 156 +++++++++++++++++++++++++ 6 files changed, 489 insertions(+) create mode 100644 actions/lint-test-build/action.yml create mode 100644 actions/node-ci/action.yml create mode 100644 actions/restore-assets/action.yml create mode 100644 actions/setup-rust/action.yml create mode 100644 actions/sync-assets/action.yml create mode 100644 workflows/project-board-automation.yml diff --git a/actions/lint-test-build/action.yml b/actions/lint-test-build/action.yml new file mode 100644 index 0000000..b5eecc6 --- /dev/null +++ b/actions/lint-test-build/action.yml @@ -0,0 +1,56 @@ +name: 'Lint, Test, and Build' +description: 'Run lint, test, and build steps for Node.js projects' + +inputs: + working-directory: + description: 'Working directory for commands' + required: true + flavor: + description: 'Build flavor (dev or prod) - lint and test only run for dev' + required: false + default: 'dev' + run-lint: + description: 'Whether to run lint step' + required: false + default: 'true' + lint-command: + description: 'Command to run for linting' + required: false + default: 'npm run lint' + run-test: + description: 'Whether to run test step' + required: false + default: 'true' + test-command: + description: 'Command to run for testing' + required: false + default: 'npm run test' + run-build: + description: 'Whether to run build step' + required: false + default: 'true' + build-command: + description: 'Command to run for building' + required: false + default: 'npm run build' + +runs: + using: 'composite' + steps: + - name: Lint + if: inputs.run-lint == 'true' && inputs.flavor == 'dev' + shell: bash + run: ${{ inputs.lint-command }} + working-directory: ${{ inputs.working-directory }} + + - name: Test + if: inputs.run-test == 'true' && inputs.flavor == 'dev' + shell: bash + run: ${{ inputs.test-command }} + working-directory: ${{ inputs.working-directory }} + + - name: Build + if: inputs.run-build == 'true' + shell: bash + run: ${{ inputs.build-command }} + working-directory: ${{ inputs.working-directory }} diff --git a/actions/node-ci/action.yml b/actions/node-ci/action.yml new file mode 100644 index 0000000..68892b4 --- /dev/null +++ b/actions/node-ci/action.yml @@ -0,0 +1,75 @@ +name: 'Node.js CI Setup' +description: 'Checkout repository, setup Node.js, cache node_modules, and install dependencies' + +inputs: + node-version: + description: 'Node.js version to use' + required: false + default: '24.13.0' + checkout-path: + description: 'Path to checkout the repository' + required: false + default: '.' + working-directory: + description: 'Working directory for npm install (relative to checkout-path)' + required: true + flavor: + description: 'Build flavor (dev or prod)' + required: false + default: 'dev' + cache-prefix: + description: 'Prefix for cache key' + required: false + default: 'node' + checkout-submodules: + description: 'Whether to checkout submodules' + required: false + default: 'false' + checkout-ref: + description: 'Git ref to checkout (branch, tag, or SHA)' + required: false + default: '' + +runs: + using: 'composite' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ inputs.checkout-path }} + ref: ${{ inputs.checkout-ref || github.ref }} + + - name: Checkout submodules + if: inputs.checkout-submodules == 'true' + shell: bash + run: git submodule update --init --recursive + working-directory: ${{ inputs.checkout-path }}/${{ inputs.working-directory }} + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ inputs.node-version }} + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + cache-dependency-path: '${{ inputs.checkout-path }}/${{ inputs.working-directory }}/package-lock.json' + + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: ${{ inputs.checkout-path }}/${{ inputs.working-directory }}/node_modules + key: ${{ runner.os }}-${{ inputs.cache-prefix }}-${{ inputs.flavor }}-node-${{ inputs.node-version }}-${{ hashFiles(format('{0}/{1}/package-lock.json', inputs.checkout-path, inputs.working-directory)) }} + restore-keys: | + ${{ runner.os }}-${{ inputs.cache-prefix }}-${{ inputs.flavor }}-node-${{ inputs.node-version }}- + ${{ runner.os }}-${{ inputs.cache-prefix }}-${{ inputs.flavor }}- + + - name: Install dependencies (dev) + if: inputs.flavor == 'dev' + shell: bash + run: npm ci + working-directory: ${{ inputs.checkout-path }}/${{ inputs.working-directory }} + + - name: Install dependencies (prod) + if: inputs.flavor == 'prod' + shell: bash + run: npm ci --omit=dev --omit=optional + working-directory: ${{ inputs.checkout-path }}/${{ inputs.working-directory }} diff --git a/actions/restore-assets/action.yml b/actions/restore-assets/action.yml new file mode 100644 index 0000000..969d75c --- /dev/null +++ b/actions/restore-assets/action.yml @@ -0,0 +1,62 @@ +name: 'Restore Cached Assets' +description: 'Restore cached mining pool and promo video assets from cache and/or artifacts' + +inputs: + frontend-path: + description: 'Path to frontend directory' + required: true + use-artifacts: + description: 'Whether to download from artifacts (requires cache job to have run first)' + required: false + default: 'true' + +outputs: + mining-pool-cache-hit: + description: 'Whether mining pool assets were found in cache' + value: ${{ steps.cache-mining-pool-restore.outputs.cache-hit }} + promo-video-cache-hit: + description: 'Whether promo video assets were found in cache' + value: ${{ steps.cache-promo-video-restore.outputs.cache-hit }} + +runs: + using: 'composite' + steps: + - name: Restore cached mining pool assets + continue-on-error: true + id: cache-mining-pool-restore + uses: actions/cache/restore@v4 + with: + path: mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Restore cached promo video assets + continue-on-error: true + id: cache-promo-video-restore + uses: actions/cache/restore@v4 + with: + path: promo-video-assets.zip + key: promo-video-assets-cache + + - name: Download mining pool artifact + if: inputs.use-artifacts == 'true' + uses: actions/download-artifact@v4 + with: + name: mining-pool-assets + continue-on-error: true + + - name: Download promo video artifact + if: inputs.use-artifacts == 'true' + uses: actions/download-artifact@v4 + with: + name: promo-video-assets + continue-on-error: true + + - name: Unzip mining pool assets + shell: bash + continue-on-error: true + run: unzip -o mining-pool-assets.zip -d ${{ inputs.frontend-path }}/src/resources/mining-pools + + - name: Unzip promo video assets + shell: bash + continue-on-error: true + run: unzip -o promo-video-assets.zip -d ${{ inputs.frontend-path }}/src/resources/promo-video diff --git a/actions/setup-rust/action.yml b/actions/setup-rust/action.yml new file mode 100644 index 0000000..9a48c10 --- /dev/null +++ b/actions/setup-rust/action.yml @@ -0,0 +1,56 @@ +name: 'Setup Rust Toolchain' +description: 'Read rust-toolchain file, cache Rust dependencies, and install the toolchain' + +inputs: + working-directory: + description: 'Working directory (repository root)' + required: true + rust-toolchain-path: + description: 'Path to rust-toolchain file relative to working-directory' + required: false + default: 'rust/gbt/rust-toolchain' + cargo-lock-path: + description: 'Path pattern for Cargo.lock files relative to working-directory' + required: false + default: 'rust/gbt/**/Cargo.lock' + rust-target-path: + description: 'Path to Rust target directory relative to working-directory' + required: false + default: 'rust/gbt/target/' + flavor: + description: 'Build flavor for cache key differentiation' + required: false + default: 'dev' + +outputs: + toolchain: + description: 'The Rust toolchain version that was installed' + value: ${{ steps.gettoolchain.outputs.toolchain }} + +runs: + using: 'composite' + steps: + - name: Read rust-toolchain file + id: gettoolchain + shell: bash + run: echo "toolchain=$(cat ./${{ inputs.rust-toolchain-path }})" >> $GITHUB_OUTPUT + working-directory: ${{ inputs.working-directory }} + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ${{ inputs.working-directory }}/${{ inputs.rust-target-path }} + key: ${{ runner.os }}-cargo-${{ inputs.flavor }}-${{ hashFiles(format('{0}/{1}', inputs.working-directory, inputs.cargo-lock-path)) }} + restore-keys: | + ${{ runner.os }}-cargo-${{ inputs.flavor }}- + ${{ runner.os }}-cargo- + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 + with: + toolchain: ${{ steps.gettoolchain.outputs.toolchain }} diff --git a/actions/sync-assets/action.yml b/actions/sync-assets/action.yml new file mode 100644 index 0000000..956db1f --- /dev/null +++ b/actions/sync-assets/action.yml @@ -0,0 +1,84 @@ +name: 'Sync and Cache Assets' +description: 'Sync assets from CDN, zip, upload as artifacts, and save to cache' + +inputs: + frontend-path: + description: 'Path to frontend directory' + required: true + github-token: + description: 'GitHub token for API access' + required: true + sync-command: + description: 'npm command to run for syncing assets' + required: false + default: 'npm run sync-assets-dev' + +runs: + using: 'composite' + steps: + - name: Restore cached mining pool assets + continue-on-error: true + id: cache-mining-pool-restore + uses: actions/cache/restore@v4 + with: + path: mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Restore cached promo video assets + continue-on-error: true + id: cache-promo-video-restore + uses: actions/cache/restore@v4 + with: + path: promo-video-assets.zip + key: promo-video-assets-cache + + - name: Unzip mining pool assets before sync + continue-on-error: true + shell: bash + run: unzip -o mining-pool-assets.zip -d ${{ inputs.frontend-path }}/src/resources/mining-pools + + - name: Unzip promo video assets before sync + continue-on-error: true + shell: bash + run: unzip -o promo-video-assets.zip -d ${{ inputs.frontend-path }}/src/resources/promo-video + + - name: Sync assets + shell: bash + run: ${{ inputs.sync-command }} + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + MEMPOOL_CDN: 1 + VERBOSE: 1 + working-directory: ${{ inputs.frontend-path }} + + - name: Zip mining pool assets + shell: bash + run: zip -jrq mining-pool-assets.zip ${{ inputs.frontend-path }}/src/resources/mining-pools/* + + - name: Zip promo video assets + shell: bash + run: zip -jrq promo-video-assets.zip ${{ inputs.frontend-path }}/src/resources/promo-video/* + + - name: Upload mining pool assets artifact + uses: actions/upload-artifact@v4 + with: + name: mining-pool-assets + path: mining-pool-assets.zip + + - name: Upload promo video assets artifact + uses: actions/upload-artifact@v4 + with: + name: promo-video-assets + path: promo-video-assets.zip + + - name: Save mining pool assets cache + uses: actions/cache/save@v4 + with: + path: mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Save promo video assets cache + uses: actions/cache/save@v4 + with: + path: promo-video-assets.zip + key: promo-video-assets-cache diff --git a/workflows/project-board-automation.yml b/workflows/project-board-automation.yml new file mode 100644 index 0000000..5c57075 --- /dev/null +++ b/workflows/project-board-automation.yml @@ -0,0 +1,156 @@ +# Reusable workflow: Automate project board management +# - Add newly created issues to project board +# - Set status to "Review Needed" when a reviewer is requested on a non-draft PR +name: Project Board Automation + +on: + workflow_call: + inputs: + project-number: + description: 'The project board number' + required: false + type: number + default: 8 + runs-on: + description: 'Runner to use for the job' + required: false + type: string + default: 'ubuntu-latest' + handle-issues: + description: 'Whether to handle adding issues to project board' + required: false + type: boolean + default: true + secrets: + PROJECT_TOKEN: + description: 'PAT with project write access' + required: true + PROJECT_ID: + description: 'The project unique identifier' + required: true + STATUS_FIELD_ID: + description: 'The Status field unique identifier' + required: true + REVIEW_NEEDED_OPTION_ID: + description: 'The Review Needed option unique identifier' + required: true + +jobs: + manage-project-board: + runs-on: ${{ inputs.runs-on }} + steps: + - name: Update Project Board + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.PROJECT_TOKEN }} + script: | + const projectNumber = ${{ inputs.project-number }}; + const handleIssues = ${{ inputs.handle-issues }}; + + // Skip draft PRs + if (context.eventName === 'pull_request' && context.payload.pull_request.draft) { + console.log('PR is a draft, skipping Review Needed status...'); + return; + } + + // Handle new issues - add to project + if (context.eventName === 'issues' && handleIssues) { + const addMutation = ` + mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { + item { id } + } + } + `; + + try { + await github.graphql(addMutation, { + projectId: "${{ secrets.PROJECT_ID }}", + contentId: context.payload.issue.node_id + }); + + console.log(`Successfully added issue to project #${projectNumber}`); + } catch (error) { + // Handle case where the issue is already in the project + const errors = error && error.errors ? error.errors : []; + const alreadyInProject = errors.some(e => + typeof e.message === 'string' && + e.message.toLowerCase().includes('already') && + e.message.toLowerCase().includes('project') + ); + + if (alreadyInProject) { + console.log(`Issue is already in project #${projectNumber}, skipping add.`); + } else { + console.error(`Failed to add issue to project #${projectNumber}:`, error); + throw error; + } + } + return; + } + + // Handle PR review_requested - update status to "Review Needed" + if (context.eventName === 'pull_request') { + // GraphQL query to find the PR's project items + const query = ` + query($owner: String!, $repo: String!, $pr: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $pr) { + projectItems(first: 10) { + nodes { + id + project { + number + } + } + } + } + } + } + `; + + // Execute the query with current repo/PR context + const result = await github.graphql(query, { + owner: context.repo.owner, + repo: context.repo.repo, + pr: context.payload.pull_request.number + }); + + // Find the project item that belongs to the target project + const projectItems = result.repository.pullRequest.projectItems.nodes; + const projectItem = projectItems.find(item => item.project.number === projectNumber); + + // Exit early if PR isn't in the target project + if (!projectItem) { + console.log(`PR is not in project #${projectNumber}, skipping...`); + return; + } + + // GraphQL mutation to update the Status field + const mutation = ` + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { + updateProjectV2ItemFieldValue( + input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: { singleSelectOptionId: $optionId } + } + ) { + projectV2Item { + id + } + } + } + `; + + // Execute the mutation + await github.graphql(mutation, { + projectId: "${{ secrets.PROJECT_ID }}", + itemId: projectItem.id, + fieldId: "${{ secrets.STATUS_FIELD_ID }}", + optionId: "${{ secrets.REVIEW_NEEDED_OPTION_ID }}" + }); + + console.log('Successfully updated project status to Review Needed'); + }