Repository navigation
fix(release): build clients natively on unprivileged runners #5068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ktechmidas
merged 3 commits into
dashpay:v4.2-dev
from
PastaPastaPasta:fix/native-protobuf-release-build
Sep 27, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,85 @@ | ||
| # NPM release runners | ||
|
|
||
| NPM release compilation uses `[self-hosted, Linux, X64, npm-build]` in the | ||
| restricted `platform-npm-releases` runner group. Publishing | ||
| continues on GitHub-hosted Ubuntu with OIDC; the builder receives no publishing | ||
| credentials. The `npm-release-build` action is shared by releases and image | ||
| validation so both compile and pack with the same setup. | ||
|
|
||
| ## Image contract | ||
|
|
||
| `.github/runner-requirements.json` pins the complete Linux image requirements and | ||
| recipe commit. The job runs `ci-image-contract verify` before compiling. The | ||
| runner must have `/opt/client-codegen` matching `packages/dapi-grpc/codegen.json`. | ||
| Its protobuf 3.18.1 compiler is intentionally separate from Rust's protoc 32.0. | ||
| TypeScript generation comes from the workspace's pinned `ts-protoc-gen` dependency. | ||
|
|
||
| Image-owned native dependencies are verified, never installed using sudo. Rust, | ||
| Node and the pinned WASM tools use writable runner/user locations. Cargo targets | ||
| remain outside the checkout; each release starts with a fresh workspace. The | ||
| runner needs no Docker CLI/socket or KVM device. | ||
|
|
||
| ## Provisioning and promotion | ||
|
|
||
| Use the reviewed `dashpay/dash-selfhosted-image` recipe and a tested immutable | ||
| image digest, not a moving tag. Register dedicated release capacity with `npm-build` only after | ||
| the NPM validation workflow succeeds on that image. Drain old registrations before | ||
| replacement; retain their image/configuration for rollback. Old release tags | ||
| still contain their original workflows and do not automatically gain this fix. | ||
|
|
||
| Requirements-changing PRs select a candidate label bound to the complete PR head | ||
| and image digest. The image controller must support the `npm` job kind and | ||
| `.github/workflows/npm-runner-validation.yml`. Manifests requesting native client | ||
| generation require successful Rust, Kotlin and NPM candidate jobs for promotion; | ||
| skipped fork jobs do not qualify. Existing same-repository/trusted-fork guards | ||
| remain in effect. | ||
|
|
||
| The trusted `runner-image-candidate.yml` bootstrap, controller and Rust/Kotlin | ||
| candidate routing must be installed on each consuming branch before candidate | ||
| promotion can work. Platform PRs #4702 and #4912 establish those pieces; reconcile | ||
| their requirements/selector files with this NPM extension when landing them. In | ||
| particular, update both the bootstrap's reusable-workflow SHA and its | ||
| `control_revision` to a reviewed image-repository revision supporting | ||
| `client_codegen` and `npm`. Merely changing `recipe_revision` is insufficient. | ||
| The default `v4.2-dev` and `v4.3-dev` branches must each use an explicit compatible | ||
| manifest; this change does not alter an existing release tag or deploy a runner. | ||
|
|
||
| ## Verification | ||
|
|
||
| `npm-runner-validation.yml` runs the real release build and packing action, DAPI | ||
| unit tests, and a byte-for-byte check that packed Node/web clients match the | ||
| freshly generated files. It uploads tarballs but never publishes them. | ||
| `test-client-codegen.yml` also builds the native compilers on hosted Linux/macOS, | ||
| checks committed generated output, tests failure recovery and validates packing. | ||
|
|
||
| Local setup and generator test commands are in `packages/dapi-grpc/README.md`. | ||
|
|
||
| ## Separate PR and release state | ||
|
|
||
| The `platform-npm-releases` organization runner group must select only the | ||
| `dashpay/platform` repository and restrict execution to: | ||
|
|
||
| ```text | ||
| dashpay/platform/.github/workflows/release-npm-build.yml@refs/heads/v4.2-dev | ||
| ``` | ||
|
|
||
| Protect that branch with the normal maintainer review policy. `release.yml` | ||
| invokes that protected reusable workflow; the reusable workflow rejects PR | ||
| callers and arbitrary branch dispatches before checkout. A PR cannot select the | ||
| release group by changing its own workflow to request the `npm-build` label. | ||
| The group-level selected-workflow restriction is a required operator setting, | ||
| not something a repository workflow can grant itself. | ||
|
|
||
| Use separate runner containers/VMs and separate registration, workspace, HOME, | ||
| Cargo registry and target-cache storage for PR and release pools. Do not mount | ||
| the same cache volumes into both pools. Release caches can persist between | ||
| releases; no PR may write them. Ordinary PR validation uses `npm-pr`; image | ||
| candidates continue to use fresh one-job registrations and volumes. Do not add | ||
| `npm-pr`, `rust-ci` or `kotlin-ci` to the release registration. | ||
|
|
||
| For another maintained branch, create its reviewed protected reusable-workflow | ||
| ref and corresponding group policy explicitly. Do not wildcard the workflow | ||
| restriction or allow PR refs. Validate the policy by attempting a PR job that | ||
| requests the release group: it must be rejected, while a permitted release dry | ||
| run succeeds. The workflow guard is defense in depth; enabling the release | ||
| pool without its group restriction does not establish this boundary. |
This file contains hidden or 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 |
|---|---|---|
|
|
@@ -9,3 +9,5 @@ self-hosted-runner: | |
| labels: | ||
| - rust-ci | ||
| - kotlin-ci | ||
| - npm-build | ||
| - npm-pr | ||
This file contains hidden or 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,121 @@ | ||
| name: Build release NPM packages | ||
| description: Compile and pack on the provisioned unprivileged Linux image | ||
| inputs: | ||
| cache-name: | ||
| description: Persistent Cargo target cache name | ||
| default: release-npm-target | ||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Verify provisioned runner dependencies | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| ci-image-contract verify .github/runner-requirements.json | ||
| python3 packages/dapi-grpc/scripts/setup-codegen.py | ||
| for pkg in build-essential cmake curl jq libgmp-dev libssl-dev pkg-config python3 unzip zip; do | ||
| dpkg-query -W -f='${Status}' "$pkg" | grep -Fx 'install ok installed' | ||
| done | ||
| test -x "$HOME/.cargo/bin/rustup" | ||
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | ||
|
|
||
| - name: Setup Rust | ||
| uses: ./.github/actions/rust | ||
| with: | ||
| target: wasm32-unknown-unknown | ||
| # The runner persists the Cargo registry between runs. | ||
| cache: 'false' | ||
| system-dependencies: verify | ||
|
|
||
| # The Rust action above reuses a protoc left in ~/.local by earlier jobs. | ||
| # Override it with a fresh, checksum-verified copy for the release build; | ||
| # prost-build reads PROTOC first. | ||
| - name: Install protoc v32.0 | ||
| env: | ||
| PROTOC_SHA256: 7ca037bfe5e5cabd4255ccd21dd265f79eb82d3c010117994f5dc81d2140ee88 | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| PROTOC_DIR="$RUNNER_TEMP/protoc-32.0" | ||
| curl -fsSL -o "$RUNNER_TEMP/protoc.zip" \ | ||
| https://github.com/protocolbuffers/protobuf/releases/download/v32.0/protoc-32.0-linux-x86_64.zip | ||
| echo "$PROTOC_SHA256 $RUNNER_TEMP/protoc.zip" | sha256sum -c - | ||
| rm -rf "$PROTOC_DIR" | ||
| unzip -q "$RUNNER_TEMP/protoc.zip" -d "$PROTOC_DIR" | ||
| echo "PROTOC=$PROTOC_DIR/bin/protoc" >> "$GITHUB_ENV" | ||
| echo "$PROTOC_DIR/bin" >> "$GITHUB_PATH" | ||
| "$PROTOC_DIR/bin/protoc" --version | ||
|
|
||
| - name: Prepare release Cargo target cache | ||
| uses: ./.github/actions/release-cargo-target-cache | ||
| with: | ||
| name: ${{ inputs.cache-name }} | ||
|
|
||
| - name: Setup Node.JS | ||
| uses: ./.github/actions/nodejs | ||
|
|
||
| - name: Install Cargo binstall | ||
| uses: cargo-bins/cargo-binstall@v1.3.1 | ||
|
|
||
| # Always reinstalled, never trusted from an earlier job: a binary left on | ||
| # this persistent runner can print the pinned version and still be | ||
| # something else. | ||
| - name: Install wasm-bindgen-cli | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| cargo binstall wasm-bindgen-cli@0.2.108 --no-confirm --force | ||
| wasm-bindgen --version | grep -Fxq 'wasm-bindgen 0.2.108' | ||
|
|
||
| - name: Install wasm-pack | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| cargo binstall wasm-pack@0.15.0 --no-confirm --force | ||
| wasm-pack --version | grep -Fxq 'wasm-pack 0.15.0' | ||
|
|
||
| # Fresh, checksum-verified copy in this job's temp directory rather than | ||
| # one left in ~/.local by an earlier job. | ||
| - name: Install Binaryen | ||
| env: | ||
| BINARYEN_SHA256: c90e0e295e8f8484ba5b47da92f26e5d1d18db6cd2fcc0c5cc265a5a73609f17 | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| ARCHIVE="$RUNNER_TEMP/binaryen-version_121-x86_64-linux.tar.gz" | ||
| curl -fsSL -o "$ARCHIVE" \ | ||
| https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-linux.tar.gz | ||
| echo "$BINARYEN_SHA256 $ARCHIVE" | sha256sum -c - | ||
| rm -rf "$RUNNER_TEMP/binaryen-version_121" | ||
| tar -xzf "$ARCHIVE" -C "$RUNNER_TEMP" | ||
| echo "$RUNNER_TEMP/binaryen-version_121/bin" >> "$GITHUB_PATH" | ||
|
|
||
| - name: Build packages | ||
| shell: bash | ||
| run: yarn build | ||
| env: | ||
| CARGO_BUILD_PROFILE: release | ||
|
|
||
| - name: Ignore only already cached artifacts | ||
| shell: bash | ||
| run: | | ||
| find . -name '.gitignore' -exec rm -f {} + | ||
| { | ||
| echo ".yarn" | ||
| echo "target" | ||
| echo "node_modules" | ||
| echo ".nyc_output" | ||
| echo ".idea" | ||
| echo ".ultra.cache.json" | ||
| echo "db/*" | ||
| echo "npm-packages" | ||
| } >> .gitignore | ||
|
|
||
| - name: Pack public workspaces | ||
| shell: bash | ||
| run: | | ||
| mkdir -p npm-packages | ||
| yarn workspaces foreach --all --no-private --parallel pack \ | ||
| --out "$GITHUB_WORKSPACE/npm-packages/%s-%v.tgz" | ||
| test -n "$(find npm-packages -maxdepth 1 -type f -name '*.tgz' -print -quit)" | ||
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,41 @@ | ||
| """Execute the protected reusable workflow's guard against caller contexts.""" | ||
| import os | ||
| from pathlib import Path | ||
| import subprocess | ||
| import unittest | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[3] | ||
|
|
||
|
|
||
| def release_guard(): | ||
| workflow = (ROOT / '.github/workflows/release-npm-build.yml').read_text() | ||
| # The guard must run before checkout or any caller-controlled source. | ||
| start = workflow.index(' - name: Reject untrusted release callers') | ||
| end = workflow.index(' - uses: softwareforgood/', start) | ||
| block = workflow[start:end] | ||
| script = block.split(' run: |\n', 1)[1] | ||
| return '\n'.join(line[10:] for line in script.splitlines() if line.strip()) | ||
|
|
||
|
|
||
| class ReleaseBoundaryTests(unittest.TestCase): | ||
| def run_guard(self, event, ref, repository='dashpay/platform'): | ||
| return subprocess.run(['bash', '-c', release_guard()], capture_output=True, text=True, | ||
| env=dict(os.environ, GITHUB_REPOSITORY=repository, | ||
| GITHUB_EVENT_NAME=event, GITHUB_REF=ref)).returncode | ||
|
|
||
| def test_should_allow_release_tags_and_protected_branch_dry_runs(self): | ||
| for event, ref in [('release', 'refs/tags/v4.2.0-beta.5'), | ||
| ('workflow_dispatch', 'refs/tags/v4.2.0-beta.5'), | ||
| ('workflow_dispatch', 'refs/heads/v4.2-dev')]: | ||
| with self.subTest(event=event, ref=ref): | ||
| self.assertEqual(self.run_guard(event, ref), 0) | ||
|
|
||
| def test_should_reject_prs_forks_and_unprotected_dispatches(self): | ||
| for event, ref in [('pull_request', 'refs/pull/5068/merge'), | ||
| ('pull_request_target', 'refs/heads/v4.2-dev'), | ||
| ('workflow_dispatch', 'refs/heads/attacker'), | ||
| ('push', 'refs/heads/v4.2-dev'), | ||
| ('release', 'refs/heads/v4.2-dev')]: | ||
| with self.subTest(event=event, ref=ref): | ||
| self.assertNotEqual(self.run_guard(event, ref), 0) | ||
| self.assertNotEqual(self.run_guard('release', 'refs/tags/v4.2.0', 'unknown/platform'), 0) |
This file contains hidden or 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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.