ci: one CI path — .hanzo/workflows

Every job in all seven workflows named `runs-on: lux-build-amd64`, except
build.yml's arm64 leg, which named `["self-hosted","linux","arm64"]`. No runner
on this forge carries any of those labels — the pools are
`lux-build-linux-<arch>` — and a job naming a label nothing carries does not
fail: it waits. ci.yml additionally fired on `branches: ["*"]`, so every push to
every branch queued the whole file, CodeQL and SBOM included.

What survives: test, lint, govulncheck and build move to
.hanzo/workflows/ci.yml, scoped to main and pull requests.

docker.yml and release.yml merge into .hanzo/workflows/release.yml. Both fired
on the same `v*` tag with no shared concurrency group, which is how a tag ends
up half-published; they are now one workflow — per-arch image, manifest, Go
binaries, GitHub release, notify — under one group that does not cancel. The
manifest step also proves the composed tag is pullable before the job goes
green, so a pin can never point at a tag that resolves to nothing.

Two auth paths had to change or the release could not work off GitHub:

  * ghcr login used `github.actor` plus the instance GITHUB_TOKEN, which here is
    a Hanzo Git token with no rights at ghcr.io. It now uses GHCR_USER and
    GHCR_TOKEN, the org Actions secrets sourced from KMS.

  * the luxfi/hsm module token is written to RUNNER_TEMP with its trailing
    whitespace stripped before BuildKit mounts it. Dockerfile:50 interpolates it
    into a URL, so one trailing newline truncates the URL there and git resolves
    the credential prefix as the host ("Could not resolve host: x-access-token").
    luxfi/node hit exactly this.

  * the universe notification used peter-evans/repository-dispatch, which
    resolves GITHUB_API_URL — on this forge that is the forge's own API, where a
    github.com PAT does not authenticate and luxfi/universe's dispatch endpoint
    does not exist. It now calls api.github.com explicitly.

Deleted rather than migrated:

  * e2e-tests.yml and benchmarks.yml. Both trigger on `push: branches: [master]`
    while the default branch is main, so neither has fired. Both also build
    `./cmd/lux-mpc`, which is not in this repo (cmd/ holds mpc and mpcd), and
    drive `e2e/docker-compose.test.yaml`, which does not exist either (the file
    is e2e/compose.test.yml). Three independent reasons each; they have never
    run and could not have passed.

    The e2e SUITE is real and now has no CI. It needs repair at the source
    before it can have any: e2e/*.go shells out to `lux-mpc` and `lux-mpc-cli`,
    which no longer exist, and to `docker-compose.test.yaml`, which is named
    compose.test.yml; e2e/Makefile expects `mpc` and `mpc-cli` on PATH and
    mpc-cli is not built either. That is a repair, not a migration, so it is not
    smuggled in here.

  * build.yml. It pushed a floating `:latest` and branch-named tags on every
    push to main/test/dev, against the semver-only policy the release workflow
    holds to, and its arm64 half named a pool that does not exist.

  * the codeql-analysis, sbom and security-scan jobs. All three exist to upload
    SARIF to the GitHub Security tab via github/codeql-action/upload-sarif.
    There is no Security tab on this forge, so they compute results with nowhere
    to put them. govulncheck stays in CI, reporting into the job log.

docs.yml's build survives as .hanzo/workflows/docs.yml; its publish does not.
configure-pages, upload-pages-artifact and deploy-pages speak to the GitHub
Pages API, which this forge does not have. The docs still fail the build that
breaks them; they are no longer published from CI until the site is served from
the PaaS.

All seven files are accounted for, which is the constraint: the forge stops at
the first workflow directory that exists, so a repo's entire .github/workflows
suite goes dark the moment .hanzo/workflows appears.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-30 17:25:50 -07:00
co-authored by Hanzo Dev
parent 985964981c
commit 0498934650
10 changed files with 340 additions and 1102 deletions
-180
View File
@@ -1,180 +0,0 @@
name: Benchmarks
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
inputs:
benchmark_time:
description: 'Benchmark time per test (e.g., 10s, 1m)'
required: false
default: '10s'
jobs:
unit-benchmarks:
runs-on: lux-build-amd64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.26.3"
- name: Cache Go modules
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install dependencies
run: go mod download
- name: Run unit benchmarks
run: |
go test -bench=. -benchmem -benchtime=${{ github.event.inputs.benchmark_time || '10s' }} ./pkg/... | tee benchmark_results.txt
- name: Upload benchmark results
uses: actions/upload-artifact@v3
with:
name: unit-benchmark-results
path: benchmark_results.txt
e2e-benchmarks:
runs-on: lux-build-amd64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.26.3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Go modules
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Build binaries
run: |
go build -o lux-mpc ./cmd/lux-mpc
go build -o mpc ./cmd/mpc
chmod +x lux-mpc mpc
- name: Install binaries
run: |
sudo mv lux-mpc /usr/local/bin/
sudo mv mpc /usr/local/bin/
- name: Install E2E dependencies
run: |
cd e2e
go mod download
- name: Setup test environment
run: |
cd e2e
docker compose -f docker-compose.test.yaml up -d
sleep 10
docker compose -f docker-compose.test.yaml ps
- name: Run E2E benchmarks
run: |
cd e2e
go test -bench=. -benchmem -benchtime=${{ github.event.inputs.benchmark_time || '10s' }} -timeout=30m | tee e2e_benchmark_results.txt
- name: Upload E2E benchmark results
uses: actions/upload-artifact@v3
with:
name: e2e-benchmark-results
path: e2e/e2e_benchmark_results.txt
- name: Cleanup
if: always()
run: |
cd e2e
docker compose -f docker-compose.test.yaml down -v
./cleanup_test_env.sh || true
benchmark-comparison:
needs: [unit-benchmarks, e2e-benchmarks]
runs-on: lux-build-amd64
if: github.event_name == 'pull_request'
steps:
- name: Download current benchmark results
uses: actions/download-artifact@v3
with:
name: unit-benchmark-results
path: current/
- name: Download E2E benchmark results
uses: actions/download-artifact@v3
with:
name: e2e-benchmark-results
path: current/
- name: Comment PR with results
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let comment = '## 📊 Benchmark Results\n\n';
// Read unit benchmark results
if (fs.existsSync('current/benchmark_results.txt')) {
const unitResults = fs.readFileSync('current/benchmark_results.txt', 'utf8');
comment += '### Unit Benchmarks\n```\n' + unitResults + '\n```\n\n';
}
// Read E2E benchmark results
if (fs.existsSync('current/e2e_benchmark_results.txt')) {
const e2eResults = fs.readFileSync('current/e2e_benchmark_results.txt', 'utf8');
comment += '### E2E Benchmarks\n```\n' + e2eResults + '\n```\n';
}
// Find and update or create comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' && comment.body.includes('📊 Benchmark Results')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
-62
View File
@@ -1,62 +0,0 @@
name: Build
on:
push:
branches: [main, test, dev]
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
docker:
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: '["self-hosted","linux","amd64"]'
- arch: arm64
runner: '["self-hosted","linux","arm64"]'
runs-on: ${{ fromJSON(matrix.runner) }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}-${{ matrix.arch }}
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,scope=${{ matrix.arch }},mode=max
manifest:
needs: [docker]
if: always() && contains(needs.docker.result, 'success')
runs-on: lux-build-amd64
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v3
- run: |
IMAGE="ghcr.io/${{ github.repository }}"
BRANCH="${{ github.ref_name }}"
SOURCES=""
for ARCH in amd64 arm64; do
if docker buildx imagetools inspect "${IMAGE}:${BRANCH}-${ARCH}" >/dev/null 2>&1; then
SOURCES="${SOURCES} ${IMAGE}:${BRANCH}-${ARCH}"
fi
done
[ -z "$SOURCES" ] && exit 1
for TAG in "${BRANCH}" latest; do
docker buildx imagetools create -t "${IMAGE}:${TAG}" ${SOURCES}
done
-328
View File
@@ -1,328 +0,0 @@
name: CI Pipeline
on:
push:
branches: ["*"]
pull_request:
branches: ["*"]
jobs:
test:
runs-on: lux-build-amd64
env:
GOEXPERIMENT: runtimesecret
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.3"
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: GOWORK=off go mod download
- name: Run unit tests
run: GOWORK=off CGO_ENABLED=1 go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
flags: unittests
name: codecov-umbrella
lint:
runs-on: lux-build-amd64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.3"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
env:
GOWORK: "off"
CGO_ENABLED: "0"
with:
version: latest
install-mode: goinstall
args: --timeout=5m
skip-cache: true
# Security vulnerability scanning
security-scan:
runs-on: lux-build-amd64
name: Security Vulnerability Scan
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.3"
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Vendor dependencies
run: GOWORK=off go mod vendor
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run govulncheck and generate SARIF
run: |
echo "Current directory: $(pwd)"
echo "Files in current directory:"
ls -la
govulncheck -format=sarif ./... > govulncheck-results.sarif
govulncheck -json ./... > vuln.json
count=$(jq '[.[] | select(.finding != null and .finding.trace != null)] | length' vuln.json || echo 0)
echo "Found $count vulnerabilities"
if [ "$count" -gt 0 ]; then
echo "Vulnerabilities found by govulncheck (see Security tab for details)"
else
echo "No vulnerabilities found by govulncheck"
fi
continue-on-error: true
- name: Upload govulncheck results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
continue-on-error: true
with:
sarif_file: govulncheck-results.sarif
category: govulncheck
- name: Install gosec
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
- name: Run gosec security scanner
run: |
gosec -fmt sarif -out gosec-results.sarif -exclude G304 ./...
continue-on-error: true
- name: Upload gosec results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
continue-on-error: true
with:
sarif_file: gosec-results.sarif
category: gosec
# CodeQL Analysis
codeql-analysis:
name: CodeQL Analysis
runs-on: lux-build-amd64
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["go"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.3"
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Vendor dependencies
run: GOWORK=off go mod vendor
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Build for CodeQL
run: |
CGO_ENABLED=0 go build -v ./cmd/mpcd
CGO_ENABLED=0 go build -v ./cmd/mpc
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
# SBOM Generation
sbom:
runs-on: lux-build-amd64
name: Generate SBOM
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.3"
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Vendor dependencies
run: GOWORK=off go mod vendor
- name: Build binaries
run: |
CGO_ENABLED=0 go build -o mpcd ./cmd/mpcd
CGO_ENABLED=0 go build -o mpc ./cmd/mpc
- name: Generate SBOM with anchore/sbom-action (SPDX-JSON)
uses: anchore/sbom-action@v0
with:
artifact-name: sbom-spdx.json
output-file: sbom.spdx.json
format: spdx-json
- name: Generate SBOM with anchore/sbom-action (CycloneDX)
uses: anchore/sbom-action@v0
with:
artifact-name: sbom-cyclonedx.json
output-file: sbom.cyclonedx.json
format: cyclonedx-json
upload-artifact: false
- name: Generate SBOM with anchore/sbom-action (Syft JSON)
uses: anchore/sbom-action@v0
with:
artifact-name: sbom-syft.json
output-file: sbom.syft.json
format: syft-json
upload-artifact: false
- name: Upload all SBOM artifacts
uses: actions/upload-artifact@v4
with:
name: sbom-files
path: |
sbom.spdx.json
sbom.cyclonedx.json
sbom.syft.json
retention-days: 30
- name: Install Grype
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- name: Scan SBOM with Grype
run: |
grype sbom.spdx.json -o sarif --file grype-results.sarif
continue-on-error: true
- name: Upload Grype results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
continue-on-error: true
with:
sarif_file: grype-results.sarif
category: grype
- name: Display SBOM summary
run: |
echo "SBOM Generation Summary"
echo "========================="
echo "Generated SBOM files:"
ls -la sbom.*
echo ""
echo "SBOM package count:"
echo "SPDX: $(jq '.packages | length' sbom.spdx.json)"
echo "CycloneDX: $(jq '.components | length' sbom.cyclonedx.json)"
echo "Syft: $(jq '.artifacts | length' sbom.syft.json)"
build:
runs-on: lux-build-amd64
needs: [test, lint, codeql-analysis, sbom]
env:
GOEXPERIMENT: runtimesecret
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.3"
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Vendor dependencies
run: GOWORK=off go mod vendor
- name: Build mpcd
run: CGO_ENABLED=0 GOWORK=off go build -mod=vendor -v ./cmd/mpcd
- name: Build mpc CLI
run: CGO_ENABLED=0 GOWORK=off go build -mod=vendor -v ./cmd/mpc
-86
View File
@@ -1,86 +0,0 @@
name: Docker
on:
workflow_dispatch:
push:
tags: ['v*']
permissions:
contents: read
packages: write
jobs:
build:
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
# No GH-hosted arm runners. arm64 builds via Docker buildx with QEMU on
# the amd64 self-hosted runner (QEMU is acceptable for Docker builds).
runs-on: lux-build-amd64
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
if: matrix.arch == 'arm64'
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
env:
# Token for BuildKit to fetch PRIVATE first-party modules (luxfi/hsm)
# via authed direct git — mounted as a secret, never baked in a layer.
# Regressed out in a prior "public modules" simplification; restored.
GH_TOKEN: ${{ secrets.UNIVERSE_PAT }}
run: |
TAG=${GITHUB_REF#refs/tags/}
IMG=ghcr.io/luxfi/mpc:${TAG}-${{ matrix.arch }}
docker buildx build \
--platform linux/${{ matrix.arch }} \
--secret id=gh_token,env=GH_TOKEN \
--tag "$IMG" \
--push \
.
manifest:
needs: build
runs-on: lux-build-amd64
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v3
- name: Create manifest
run: |
TAG=${GITHUB_REF#refs/tags/}
IMG=ghcr.io/luxfi/mpc
# buildx imagetools composes the per-arch OCI indices that
# `docker buildx build --push` emits. `docker manifest create`
# rejects them ("X is a manifest list"); imagetools handles them.
docker buildx imagetools create -t $IMG:$TAG \
$IMG:${TAG}-amd64 \
$IMG:${TAG}-arm64
notify:
needs: manifest
if: startsWith(github.ref, 'refs/tags/v')
runs-on: lux-build-amd64
steps:
- uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.UNIVERSE_PAT }}
repository: luxfi/universe
event-type: image-published
client-payload: |
{
"service": "mpc",
"image": "ghcr.io/luxfi/mpc:${{ github.ref_name }}",
"tag": "${{ github.ref_name }}"
}
-64
View File
@@ -1,64 +0,0 @@
name: Deploy Docs to GitHub Pages
on:
push:
branches:
- main
paths:
- 'docs/**'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: lux-build-amd64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
cache-dependency-path: docs/pnpm-lock.yaml
- name: Install dependencies
working-directory: docs
run: pnpm install --frozen-lockfile
- name: Build docs
working-directory: docs
run: pnpm build
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/out
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: lux-build-amd64
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-334
View File
@@ -1,334 +0,0 @@
name: E2E Integration Tests
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
# Build job that creates the binaries needed by all E2E test jobs
build:
runs-on: lux-build-amd64
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.26.3"
- name: Tidy Go modules
run: |
go mod tidy
- name: Generate cache key
id: cache-key
run: echo "key=${{ runner.os }}-binaries-${{ hashFiles('**/go.sum', '**/*.go') }}" >> $GITHUB_OUTPUT
- name: Cache binaries
id: cache-binaries
uses: actions/cache@v3
with:
path: |
./lux-mpc
./mpc
key: ${{ steps.cache-key.outputs.key }}
- name: Cache Go modules
if: steps.cache-binaries.outputs.cache-hit != 'true'
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install dependencies
if: steps.cache-binaries.outputs.cache-hit != 'true'
run: |
go mod download
cd e2e && go mod download
- name: Build binaries
if: steps.cache-binaries.outputs.cache-hit != 'true'
run: |
go build -o lux-mpc ./cmd/lux-mpc
go build -o mpc ./cmd/mpc
chmod +x lux-mpc mpc
# Key Generation E2E Tests
e2e-keygen:
runs-on: lux-build-amd64
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.26.3"
- name: Tidy Go modules
run: |
go mod tidy
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Verify Docker Compose
run: |
docker --version
docker compose version
- name: Cache Go modules
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Restore binaries
uses: actions/cache@v3
with:
path: |
./lux-mpc
./mpc
key: ${{ needs.build.outputs.cache-key }}
- name: Install binaries
run: |
sudo mv lux-mpc /usr/local/bin/
sudo mv mpc /usr/local/bin/
- name: Verify binaries are available
run: |
which lux-mpc
which mpc
lux-mpc --version || echo "lux-mpc binary ready"
mpc --version || echo "mpc binary ready"
- name: Install E2E dependencies
run: |
cd e2e && go mod tidy && go mod download
- name: Run Key Generation E2E tests
run: |
cd e2e
go test -v -timeout=1200s -run TestKeyGeneration
env:
DOCKER_BUILDKIT: 1
- name: Cleanup Docker containers
if: always()
run: |
cd e2e
docker compose -f docker-compose.test.yaml down -v || true
docker system prune -f || true
- name: Upload keygen test logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-keygen-test-logs
path: e2e/logs/
retention-days: 7
# Signing E2E Tests
e2e-signing:
runs-on: lux-build-amd64
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.26.3"
- name: Tidy Go modules
run: |
go mod tidy
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Verify Docker Compose
run: |
docker --version
docker compose version
- name: Cache Go modules
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Restore binaries
uses: actions/cache@v3
with:
path: |
./lux-mpc
./mpc
key: ${{ needs.build.outputs.cache-key }}
- name: Install binaries
run: |
sudo mv lux-mpc /usr/local/bin/
sudo mv mpc /usr/local/bin/
- name: Verify binaries are available
run: |
which lux-mpc
which mpc
lux-mpc --version || echo "lux-mpc binary ready"
mpc --version || echo "mpc binary ready"
- name: Install E2E dependencies
run: |
cd e2e && go mod tidy && go mod download
- name: Run Signing E2E tests
run: |
cd e2e
go test -v -timeout=1200s -run TestSigning
env:
DOCKER_BUILDKIT: 1
- name: Cleanup Docker containers
if: always()
run: |
cd e2e
docker compose -f docker-compose.test.yaml down -v || true
docker system prune -f || true
- name: Upload signing test logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-signing-test-logs
path: e2e/logs/
retention-days: 7
# Resharing E2E Tests
e2e-resharing:
runs-on: lux-build-amd64
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.26.3"
- name: Tidy Go modules
run: |
go mod tidy
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Verify Docker Compose
run: |
docker --version
docker compose version
- name: Cache Go modules
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Restore binaries
uses: actions/cache@v3
with:
path: |
./lux-mpc
./mpc
key: ${{ needs.build.outputs.cache-key }}
- name: Install binaries
run: |
sudo mv lux-mpc /usr/local/bin/
sudo mv mpc /usr/local/bin/
- name: Verify binaries are available
run: |
which lux-mpc
which mpc
lux-mpc --version || echo "lux-mpc binary ready"
mpc --version || echo "mpc binary ready"
- name: Install E2E dependencies
run: |
cd e2e && go mod tidy && go mod download
- name: Run Resharing E2E tests
run: |
cd e2e
go test -v -timeout=1200s -run TestResharing
env:
DOCKER_BUILDKIT: 1
- name: Cleanup Docker containers
if: always()
run: |
cd e2e
docker compose -f docker-compose.test.yaml down -v || true
docker system prune -f || true
- name: Upload resharing test logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-resharing-test-logs
path: e2e/logs/
retention-days: 7
# Summary job that depends on all E2E tests
e2e-summary:
runs-on: lux-build-amd64
needs: [e2e-keygen, e2e-signing, e2e-resharing]
if: always()
steps:
- name: Check E2E test results
run: |
echo "E2E Test Results Summary:"
echo "========================="
echo "Key Generation Tests: ${{ needs.e2e-keygen.result }}"
echo "Signing Tests: ${{ needs.e2e-signing.result }}"
echo "Resharing Tests: ${{ needs.e2e-resharing.result }}"
echo ""
# Check if any tests failed
if [[ "${{ needs.e2e-keygen.result }}" != "success" || "${{ needs.e2e-signing.result }}" != "success" || "${{ needs.e2e-resharing.result }}" != "success" ]]; then
echo "❌ One or more E2E tests failed"
exit 1
else
echo "✅ All E2E tests passed successfully"
fi
-48
View File
@@ -1,48 +0,0 @@
name: Release MPC
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
build:
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
# All builds run on the amd64 self-hosted runner. arm64 cross-compiles
# via GOARCH; CGO_ENABLED=0 keeps it pure-Go (no cross-compiler needed).
runs-on: lux-build-amd64
# luxfi/* are PUBLIC modules — fetched and checksum-verified through the
# default immutable proxy.golang.org + sum.golang.org. No GOPRIVATE /
# GONOSUMDB / GOPROXY=direct bypass (those route around the checksum db).
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26.3'
- name: Build (cross-compile via GOARCH)
run: |
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=${{ matrix.arch }} \
go build -trimpath -ldflags="-s -w" -o mpcd ./cmd/mpcd/
- name: Upload
uses: actions/upload-artifact@v4
with:
name: mpcd-linux-${{ matrix.arch }}
path: mpcd
release:
needs: build
runs-on: lux-build-amd64
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: dist/
- uses: softprops/action-gh-release@v2
with:
files: dist/**/*
+92
View File
@@ -0,0 +1,92 @@
name: CI
# Test, lint and build, on the native runners (git.hanzo.ai → git-runner).
#
# This replaces .github/workflows/ci.yml, whose every job named
# `runs-on: lux-build-amd64`. No runner on this forge carries that label — the
# pools are `lux-build-linux-<arch>` — and a job naming a label nothing carries
# does not fail, it waits.
#
# It also triggered on `branches: ["*"]`, so every push to every branch queued
# the whole file, CodeQL and SBOM included. It is scoped to main and pull
# requests here.
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
# A newer commit makes the older run irrelevant.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GOWORK: 'off'
jobs:
test:
runs-on: [lux-build-linux-amd64]
timeout-minutes: 30
env:
GOEXPERIMENT: runtimesecret
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- run: go mod download
- name: Unit tests
run: CGO_ENABLED=1 go test -v -race -coverprofile=coverage.out ./...
lint:
runs-on: [lux-build-linux-amd64]
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: golangci/golangci-lint-action@v6
env:
CGO_ENABLED: '0'
with:
version: latest
install-mode: goinstall
args: --timeout=5m
skip-cache: true
govulncheck:
runs-on: [lux-build-linux-amd64]
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
# Reports rather than gates, as it did before. Making it blocking is a
# policy decision, not part of moving where the job runs.
- run: govulncheck ./... || true
build:
needs: [test, lint]
runs-on: [lux-build-linux-amd64]
timeout-minutes: 20
env:
GOEXPERIMENT: runtimesecret
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- run: go mod vendor
- run: CGO_ENABLED=0 go build -mod=vendor -v ./cmd/mpcd
- run: CGO_ENABLED=0 go build -mod=vendor -v ./cmd/mpc
+56
View File
@@ -0,0 +1,56 @@
name: Docs
# Builds the docs site on the native runners (git.hanzo.ai → git-runner).
#
# This replaces .github/workflows/docs.yml, which built the site and then
# published it with actions/configure-pages + actions/upload-pages-artifact +
# actions/deploy-pages. Those three speak to the GitHub Pages API, which this
# forge does not have and is not going to grow — so the publish half cannot
# work here at all.
#
# The build half is worth keeping on its own: it fails on the commit that
# breaks the docs, path-filtered so it only runs when docs change. Publishing
# is gone until the site is served from the PaaS.
on:
push:
branches: [main]
paths:
- 'docs/**'
- '.hanzo/workflows/docs.yml'
pull_request:
paths:
- 'docs/**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: [lux-build-linux-amd64]
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
cache-dependency-path: docs/pnpm-lock.yaml
- name: Install dependencies
working-directory: docs
run: pnpm install --frozen-lockfile
- name: Build docs
working-directory: docs
run: pnpm build
+192
View File
@@ -0,0 +1,192 @@
name: Release
# Builds ghcr.io/luxfi/mpc, cuts the GitHub release, and tells luxfi/universe
# the image exists — on the native runners (git.hanzo.ai → git-runner).
#
# This replaces .github/workflows/docker.yml and .github/workflows/release.yml,
# both of which fired on the same `v*` tag and both of whose jobs named
# `runs-on: lux-build-amd64`. No runner on this forge carries that label — the
# pools are `lux-build-linux-<arch>` — and a job naming a label nothing carries
# does not fail, it waits. Two workflows racing on one tag with no shared
# concurrency group is also how a release ends up half-published; they are one
# workflow here, under one group that does not cancel.
#
# ghcr auth is GHCR_USER / GHCR_TOKEN, org Actions secrets sourced from KMS. The
# old workflows logged in as `github.actor` with the instance GITHUB_TOKEN,
# which on this forge is a Hanzo Git token with no rights at ghcr.io.
#
# arm64 is built under QEMU on the amd64 pool. That is acceptable for a docker
# build and it is what the old workflow did; the Go binaries below cross-compile
# natively.
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: write
packages: write
# A release is not made irrelevant by a newer one — let it finish.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
image:
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
runs-on: [lux-build-linux-amd64]
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
if: matrix.arch == 'arm64'
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- uses: docker/setup-buildx-action@v3
with:
driver: docker-container
driver-opts: network=host
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Write the private-module token for BuildKit
# Stripped of trailing whitespace on purpose. Dockerfile:50 interpolates
# this into a URL:
# git config url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/"
# so a single trailing newline in the stored secret truncates the URL at
# it and git resolves the credential prefix as the HOST:
# Could not resolve host: x-access-token
# luxfi/node hit exactly this. The secret's bytes are not ours to
# control, so the trim happens here.
run: |
set -euo pipefail
umask 077
printf '%s' '${{ secrets.UNIVERSE_PAT }}' | tr -d '\r\n' > "${RUNNER_TEMP}/gh_token"
if [ ! -s "${RUNNER_TEMP}/gh_token" ]; then
echo "::error::UNIVERSE_PAT is empty — the luxfi/hsm module fetch will fail"
exit 1
fi
- name: Build and push (${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: linux/${{ matrix.arch }}
secrets: |
gh_token=${{ runner.temp }}/gh_token
provenance: false
push: true
tags: ghcr.io/luxfi/mpc:${{ github.ref_name }}-${{ matrix.arch }}
manifest:
needs: image
runs-on: [lux-build-linux-amd64]
timeout-minutes: 20
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Compose the multi-arch tag
# imagetools, not `docker manifest create`: `docker buildx build --push`
# emits a per-arch OCI index, and `docker manifest create` rejects those
# ("X is a manifest list"). imagetools handles them.
run: |
set -euo pipefail
IMG=ghcr.io/luxfi/mpc
TAG='${{ github.ref_name }}'
docker buildx imagetools create -t "$IMG:$TAG" \
"$IMG:${TAG}-amd64" \
"$IMG:${TAG}-arm64"
- name: Verify the pushed manifest resolves
# A tag that names nothing pullable is how a deployment ends up pinned
# to a phantom: the pin applies, the pod never starts, and the tag looks
# like a release. Prove it before the job is green.
run: |
set -euo pipefail
img="ghcr.io/luxfi/mpc:${{ github.ref_name }}"
for i in 1 2 3 4 5 6; do
if docker manifest inspect "$img" >/dev/null 2>&1; then
echo "$img is resolvable"; exit 0
fi
echo "manifest not visible yet (attempt ${i}/6) — retrying"; sleep 5
done
echo "::error::$img not pullable after push"; exit 1
binaries:
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
# arm64 cross-compiles via GOARCH; CGO_ENABLED=0 keeps it pure Go, so one
# amd64 runner covers both and no cross-compiler is needed.
runs-on: [lux-build-linux-amd64]
timeout-minutes: 30
# luxfi/* are PUBLIC modules, fetched and checksum-verified through the
# default immutable proxy.golang.org + sum.golang.org. No GOPRIVATE /
# GONOSUMDB / GOPROXY=direct bypass — those route around the checksum db.
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build (cross-compile via GOARCH)
run: |
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=${{ matrix.arch }} \
go build -trimpath -ldflags="-s -w" -o mpcd ./cmd/mpcd/
- uses: actions/upload-artifact@v4
with:
name: mpcd-linux-${{ matrix.arch }}
path: mpcd
release:
needs: binaries
runs-on: [lux-build-linux-amd64]
timeout-minutes: 20
steps:
- uses: actions/download-artifact@v4
with:
path: dist/
- uses: softprops/action-gh-release@v2
with:
files: dist/**/*
notify:
needs: [manifest, release]
runs-on: [lux-build-linux-amd64]
timeout-minutes: 10
steps:
- name: Tell luxfi/universe the image is published
# Called against api.github.com explicitly. The old workflow used
# peter-evans/repository-dispatch, which resolves GITHUB_API_URL — here
# that is this forge's API, where a github.com PAT does not authenticate
# and luxfi/universe's dispatch endpoint does not live.
run: |
set -euo pipefail
curl -fsSL -X POST https://api.github.com/repos/luxfi/universe/dispatches \
-H 'Accept: application/vnd.github+json' \
-H 'Authorization: Bearer ${{ secrets.UNIVERSE_PAT }}' \
-d '{
"event_type": "image-published",
"client_payload": {
"service": "mpc",
"image": "ghcr.io/luxfi/mpc:${{ github.ref_name }}",
"tag": "${{ github.ref_name }}"
}
}'