diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7bbb32f..bb19e99 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,4 +9,13 @@ updates: interval: 'weekly' allow: # Starting slow by only checking our own packages for updates. - - dependency-name: '@metaplex-foundation/*' \ No newline at end of file + - dependency-name: '@metaplex-foundation/*' + + # Shared CI workflows and third-party actions are SHA-pinned; keep them current. + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' + groups: + actions: + patterns: ['*'] diff --git a/.github/file-filters.yml b/.github/file-filters.yml index a75ddb0..df26128 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -5,11 +5,11 @@ # Programs. program_common: &program_common - - ".github/workflows/build-programs.yml" - - ".github/workflows/test-programs.yml" - ".github/workflows/main.yml" - ".github/file-filters.yml" + - ".github/programs.json" - ".github/.env" + - "configs/scripts/program/**" mpl_agent_identity_program: &mpl_agent_identity_program - *program_common @@ -39,21 +39,18 @@ client_common: &client_common - *programs - ".github/workflows/main.yml" - ".github/file-filters.yml" + - ".github/programs.json" - ".github/.env" + - "configs/scripts/client/**" - "configs/shank.cjs" - - "configs/kinobi.cjs" + - "configs/kinobi-*.cjs" js_client: &js_client - *client_common - - ".github/workflows/test-js-client.yml" - - ".github/workflows/publish-js-client.yml" - "clients/js/**" rust_client: &rust_client - *client_common - - ".github/workflows/test-rust-client.yml" - - ".github/workflows/build-rust-client.yml" - - ".github/workflows/publish-rust-client.yml" - "clients/rust-identity/**" - "clients/rust-reputation/**" - "clients/rust-tools/**" diff --git a/.github/programs.json b/.github/programs.json new file mode 100644 index 0000000..98e8c53 --- /dev/null +++ b/.github/programs.json @@ -0,0 +1,125 @@ +{ + "$comment": "Program descriptors for the shared CI in metaplex-foundation/actions. Toolchain pins live in .github/.env.", + "programs": [ + { + "name": "mpl-agent-identity", + "build": "sbf", + "deploy": { + "program_id": "1DREGFgysWYxLnRnKQnwrxnJQeSMk2HmGaC6whw2B2p", + "clusters": { + "devnet": { + "type": "direct" + }, + "mainnet-beta": { + "type": "direct" + }, + "sonic-devnet": { + "type": "direct" + }, + "sonic-testnet": { + "type": "direct" + }, + "eclipse-devnet": { + "type": "direct" + }, + "eclipse-testnet": { + "type": "direct" + }, + "eclipse-mainnet": { + "type": "direct" + } + } + } + }, + { + "name": "mpl-agent-reputation", + "build": "sbf", + "deploy": { + "program_id": "REPREG5c1gPHuHukEyANpksLdHFaJCiTrm6zJgNhRZR", + "clusters": { + "devnet": { + "type": "direct" + }, + "mainnet-beta": { + "type": "direct" + }, + "sonic-devnet": { + "type": "direct" + }, + "sonic-testnet": { + "type": "direct" + }, + "eclipse-devnet": { + "type": "direct" + }, + "eclipse-testnet": { + "type": "direct" + }, + "eclipse-mainnet": { + "type": "direct" + } + } + } + }, + { + "name": "mpl-agent-validation", + "build": "sbf", + "deploy": { + "program_id": "VALREGY66A9ieJfFUNs5GrxFTy498KUoSU7TbmSePQi", + "clusters": { + "devnet": { + "type": "direct" + }, + "mainnet-beta": { + "type": "direct" + }, + "sonic-devnet": { + "type": "direct" + }, + "sonic-testnet": { + "type": "direct" + }, + "eclipse-devnet": { + "type": "direct" + }, + "eclipse-testnet": { + "type": "direct" + }, + "eclipse-mainnet": { + "type": "direct" + } + } + } + }, + { + "name": "mpl-agent-tools", + "build": "sbf", + "deploy": { + "program_id": "TLREGni9ZEyGC3vnPZtqUh95xQ8oPqJSvNjvB7FGK8S", + "clusters": { + "devnet": { + "type": "direct" + }, + "mainnet-beta": { + "type": "direct" + }, + "sonic-devnet": { + "type": "direct" + }, + "sonic-testnet": { + "type": "direct" + }, + "eclipse-devnet": { + "type": "direct" + }, + "eclipse-testnet": { + "type": "direct" + }, + "eclipse-mainnet": { + "type": "direct" + } + } + } + } + ] +} diff --git a/.github/workflows/benchmark-summary.yml b/.github/workflows/benchmark-summary.yml deleted file mode 100644 index bde1bd9..0000000 --- a/.github/workflows/benchmark-summary.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Benchmark -on: - workflow_call: - inputs: - git_ref: - type: string - -permissions: - # deployments permission to deploy GitHub pages website - deployments: write - # contents permission to update benchmark contents in gh-pages branch - contents: write - # allow posting comments to pull request - pull-requests: write - -env: - CACHE: true - -jobs: - benchmark_summary: - name: Performance regression summary - runs-on: ubuntu-latest - steps: - - name: Git checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.git_ref }} - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Sanitize Ref - id: sanitize - shell: bash - run: | - REF="${{ inputs.git_ref }}" - if [ -z "$REF" ]; then - REF="default" - fi - SANITIZED=${REF//\//-} - echo "sanitized=$SANITIZED" >> "$GITHUB_OUTPUT" - - - name: Start validator - uses: metaplex-foundation/actions/start-validator@v1 - with: - node: 18.x - solana: ${{ env.SOLANA_VERSION }} - cache: ${{ env.CACHE }} - artifacts: program-builds-${{ steps.sanitize.outputs.sanitized }} - - - name: Install dependencies - uses: metaplex-foundation/actions/install-node-dependencies@v1 - with: - folder: ./clients/js - cache: ${{ env.CACHE }} - key: clients-js - - - name: Build - working-directory: ./clients/js - run: pnpm build - - - name: Test - working-directory: ./clients/js - run: pnpm bench - - # Download previous benchmark result from cache (if exists) - - name: Download previous benchmark data - uses: actions/cache@v4 - with: - path: ./cache - key: ${{ runner.os }}-benchmark - - # Run `github-action-benchmark` action - - name: Store benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - # What benchmark tool the output.json came from - tool: "customSmallerIsBetter" - # Where the output from the benchmark tool is stored - output-file-path: ./clients/js/output.json - # Where the previous data file is stored - # external-data-json-path: ./cache/benchmark-data.json - # Enable Job Summary for PRs - summary-always: true - # Always comment on the PR - comment-always: true - # Don't save the benchmark data to the cache - save-data-file: false - # Workflow will fail when an alert happens - fail-on-alert: true - # Access token to deploy GitHub Pages branch - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 index 91361e0..0000000 --- a/.github/workflows/benchmark.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Benchmark -on: - workflow_call: - inputs: - git_ref: - type: string - -permissions: - # deployments permission to deploy GitHub pages website - deployments: write - # contents permission to update benchmark contents in gh-pages branch - contents: write - -env: - CACHE: true - -jobs: - benchmark: - name: Performance regression check - runs-on: ubuntu-latest - steps: - - name: Git checkout - uses: actions/checkout@v4 - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Sanitize Ref - id: sanitize - shell: bash - run: | - REF="${{ inputs.git_ref }}" - if [ -z "$REF" ]; then - REF="default" - fi - SANITIZED=${REF//\//-} - echo "sanitized=$SANITIZED" >> "$GITHUB_OUTPUT" - - - name: Start validator - uses: metaplex-foundation/actions/start-validator@v1 - with: - node: 18.x - solana: ${{ env.SOLANA_VERSION }} - cache: ${{ env.CACHE }} - artifacts: program-builds-${{ steps.sanitize.outputs.sanitized }} - - - name: Install dependencies - uses: metaplex-foundation/actions/install-node-dependencies@v1 - with: - folder: ./clients/js - cache: ${{ env.CACHE }} - key: clients-js - - - name: Build - working-directory: ./clients/js - run: pnpm build - - - name: Test - working-directory: ./clients/js - run: pnpm bench - - # Download previous benchmark result from cache (if exists) - - name: Download previous benchmark data - uses: actions/cache@v4 - with: - path: ./cache - key: ${{ runner.os }}-benchmark - - # Run `github-action-benchmark` action - - name: Store benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - # What benchmark tool the output.json came from - tool: "customSmallerIsBetter" - # Where the output from the benchmark tool is stored - output-file-path: ./clients/js/output.json - # Where the previous data file is stored - # external-data-json-path: ./cache/benchmark-data.json - # Workflow will fail when an alert happens - fail-on-alert: true - # Access token to deploy GitHub Pages branch - github-token: ${{ secrets.GITHUB_TOKEN }} - # Push and deploy GitHub pages branch automatically - auto-push: true diff --git a/.github/workflows/build-programs.yml b/.github/workflows/build-programs.yml deleted file mode 100644 index 5a7e379..0000000 --- a/.github/workflows/build-programs.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Build Programs - -on: - workflow_call: - inputs: - rust: - type: string - solana: - type: string - git_ref: - type: string - workflow_dispatch: - inputs: - rust: - description: Rust version - default: 1.83.0 - required: true - type: string - solana: - description: Solana version - default: 2.2.1 - required: true - type: string - -env: - CACHE: true - -jobs: - build_programs: - name: Build - runs-on: ubuntu-latest - steps: - - name: Git checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.git_ref }} - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Install Rust - uses: metaplex-foundation/actions/install-rust@v1 - with: - toolchain: ${{ inputs.rust || env.RUST_VERSION }} - - - name: Install Solana - uses: metaplex-foundation/actions/install-solana@v1 - with: - version: ${{ inputs.solana || env.SOLANA_VERSION }} - cache: ${{ env.CACHE }} - - - name: Install Protobuf Compiler - run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - - name: Cache program dependencies - if: env.CACHE == 'true' - uses: metaplex-foundation/actions/cache-programs@v1 - - - name: Build programs - shell: bash - working-directory: configs/scripts/program - run: ./build.sh - env: - PROGRAMS: ${{ env.PROGRAMS }} - - - name: Sanitize Ref - id: sanitize - shell: bash - run: | - REF="${{ inputs.git_ref }}" - if [ -z "$REF" ]; then - REF="default" - fi - SANITIZED=${REF//\//-} - echo "sanitized=$SANITIZED" >> "$GITHUB_OUTPUT" - - - name: Upload program builds - uses: actions/upload-artifact@v4 - with: - name: program-builds-${{ steps.sanitize.outputs.sanitized }} - # First wildcard ensures exported paths are consistently under the programs folder. - path: ./program*/.bin/*.so - include-hidden-files: true - if-no-files-found: error diff --git a/.github/workflows/build-rust-client.yml b/.github/workflows/build-rust-client.yml deleted file mode 100644 index ac014ab..0000000 --- a/.github/workflows/build-rust-client.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Build Rust Client - -on: - workflow_call: - inputs: - rust: - type: string - solana: - type: string - git_ref: - type: string - workflow_dispatch: - inputs: - rust: - description: Rust version - default: 1.83.0 - required: true - type: string - solana: - description: Solana version - default: 2.2.1 - required: true - type: string - -env: - CACHE: true - -jobs: - build_sdk: - name: Build - runs-on: ubuntu-latest - strategy: - matrix: - client: [rust-identity, rust-reputation, rust-validation, rust-tools] - steps: - - name: Git checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.git_ref }} - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Install Rust - uses: metaplex-foundation/actions/install-rust@v1 - with: - toolchain: ${{ inputs.rust || env.RUST_VERSION }} - - - name: Install Solana - uses: metaplex-foundation/actions/install-solana@v1 - with: - version: ${{ inputs.solana || env.SOLANA_VERSION }} - cache: ${{ env.CACHE }} - - - name: Install Protobuf Compiler - run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - - name: Cache Rust client test dependencies - uses: metaplex-foundation/actions/cache-crate@v1 - with: - folder: ./clients/${{ matrix.client }} - key: ${{ matrix.client }}-test - - - name: Run cargo clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-targets --all-features --no-deps --manifest-path ./clients/${{ matrix.client }}/Cargo.toml - - - name: Build Rust client - shell: bash - working-directory: clients/${{ matrix.client }} - run: cargo build --all-features --release - - - name: Upload Rust client builds - uses: actions/upload-artifact@v4 - with: - name: rust-client-builds-${{ matrix.client }} - path: ./targe*/release/*mpl_agent* - if-no-files-found: error diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index b1c869e..71dd399 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -8,151 +8,106 @@ on: required: true default: mpl-agent-identity type: choice - options: - - mpl-agent-identity - - mpl-agent-reputation - - mpl-agent-validation - - mpl-agent-tools + options: [mpl-agent-identity, mpl-agent-reputation, mpl-agent-validation, mpl-agent-tools] bump: description: Version bump required: true default: patch type: choice - options: - - patch - - minor - - major + options: [patch, minor, major] git_ref: - description: Commit hash or branch to create release + description: Commit hash or branch to create the release from required: false type: string default: main +permissions: {} -env: - CACHE: true +concurrency: + # One release per program at a time. + group: create-release-${{ inputs.program }} + cancel-in-progress: false jobs: + check_tag: + name: Check tag + runs-on: ubuntu-latest + permissions: + # Resolves the ref to a commit. + contents: read + outputs: + sha: ${{ steps.set_type.outputs.sha }} + steps: + - name: Check tag + id: set_type + env: + GIT_REF: ${{ inputs.git_ref }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Resolve the branch to one commit: build, test and the release tag must + # all refer to the same source, even if the branch moves meanwhile. + if [[ "$GIT_REF" == release/*@* ]]; then + echo "::error::a release is cut from a commit or branch, not from the release tag '$GIT_REF'" + exit 1 + fi + if [ -z "$GIT_REF" ]; then + SHA="$GITHUB_SHA" + else + SHA="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GIT_REF}" --jq .sha)" \ + || { echo "::error::'$GIT_REF' is not a commit, branch or tag of this repository"; exit 1; } + fi + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "$GIT_REF -> $SHA" + build_programs: name: Programs - uses: ./.github/workflows/build-programs.yml - secrets: inherit + needs: check_tag + uses: metaplex-foundation/actions/.github/workflows/build-programs.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read with: - git_ref: ${{ inputs.git_ref }} + git_ref: ${{ needs.check_tag.outputs.sha }} + apt_packages: protobuf-compiler test_programs: name: Programs - uses: ./.github/workflows/test-programs.yml - secrets: inherit + needs: [check_tag, build_programs] + uses: metaplex-foundation/actions/.github/workflows/test-programs.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read with: program_matrix: '["${{ inputs.program }}"]' - git_ref: ${{ inputs.git_ref }} + git_ref: ${{ needs.check_tag.outputs.sha }} + apt_packages: protobuf-compiler test_js: name: JS client - needs: build_programs - uses: ./.github/workflows/test-js-client.yml - secrets: inherit + needs: [check_tag, build_programs] + uses: metaplex-foundation/actions/.github/workflows/test-js-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read with: - git_ref: ${{ inputs.git_ref }} - + git_ref: ${{ needs.check_tag.outputs.sha }} + node_matrix: '["22.x", "24.x"]' + test_rust: name: Rust client - needs: build_programs - uses: ./.github/workflows/test-rust-client.yml - secrets: inherit + needs: [check_tag, build_programs] + uses: metaplex-foundation/actions/.github/workflows/test-rust-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read with: - git_ref: ${{ inputs.git_ref }} - + git_ref: ${{ needs.check_tag.outputs.sha }} + apt_packages: protobuf-compiler + client_dir: clients + create_release: - name: Create program release - runs-on: ubuntu-latest - needs: [test_js, test_rust, test_programs] + name: Program + needs: [check_tag, build_programs, test_js, test_rust, test_programs] + uses: metaplex-foundation/actions/.github/workflows/create-release.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 permissions: contents: write - steps: - - name: Git checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.git_ref }} - - name: Bump Program Version - run: | - git fetch --tags --all - VERSION=`git tag -l --sort -version:refname "release/${{ inputs.program }}@*" | head -n 1 | sed 's|release/${{ inputs.program }}@||'` - # First release for this program: start from 0.0.0 so the bump below produces a valid semver. - if [ -z "${VERSION}" ]; then - VERSION="0.0.0" - fi - MAJOR=`echo ${VERSION} | cut -d. -f1` - MINOR=`echo ${VERSION} | cut -d. -f2` - PATCH=`echo ${VERSION} | cut -d. -f3` - - if [ "${{ inputs.bump }}" == "major" ]; then - MAJOR=$((MAJOR + 1)) - MINOR=0 - PATCH=0 - elif [ "${{ inputs.bump }}" == "minor" ]; then - MINOR=$((MINOR + 1)) - PATCH=0 - else - PATCH=$((PATCH + 1)) - fi - - PROGRAM_VERSION="${MAJOR}.${MINOR}.${PATCH}" - - echo PROGRAM_VERSION="${PROGRAM_VERSION}" >> $GITHUB_ENV - - - name: Sanitize Ref - id: sanitize - shell: bash - run: | - REF="${{ inputs.git_ref }}" - if [ -z "$REF" ]; then - REF="default" - fi - SANITIZED=${REF//\//-} - echo "sanitized=$SANITIZED" >> "$GITHUB_OUTPUT" - - - name: Download Program Builds - uses: actions/download-artifact@v4 - with: - name: program-builds-${{ steps.sanitize.outputs.sanitized }} - - - name: Identify Program - run: | - PROGRAM_NAME=$(echo "${{ inputs.program }}" | tr - _) - echo PROGRAM_NAME="${PROGRAM_NAME}" >> $GITHUB_ENV - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: release/${{ inputs.program }}@${{ env.PROGRAM_VERSION }} - release_name: ${{ inputs.program }} v${{ env.PROGRAM_VERSION }} - body: | - Release ${{ inputs.program }} v${{ env.PROGRAM_VERSION }} - draft: false - prerelease: false - - - name: Upload Release Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./programs/.bin/${{ env.PROGRAM_NAME }}_program.so - asset_name: ${{ env.PROGRAM_NAME }}_program.so - asset_content_type: application/octet-stream - - # - name: Update latest tag - # uses: actions/github-script@v5 - # with: - # script: | - # github.rest.git.createRef({ - # owner: context.repo.owner, - # repo: context.repo.repo, - # ref: 'refs/tags/release/${{ inputs.program }}@latest', - # sha: '${{ github.sha }}' - # }); \ No newline at end of file + with: + program: ${{ inputs.program }} + bump: ${{ inputs.bump }} + git_ref: ${{ needs.check_tag.outputs.sha }} diff --git a/.github/workflows/deploy-program.yml b/.github/workflows/deploy-program.yml index b214b98..8ce35fb 100644 --- a/.github/workflows/deploy-program.yml +++ b/.github/workflows/deploy-program.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: git_ref: - description: Release tag (release/@) or commit to deploy + description: Release tag (release/mpl-agent-identity@0.1.0) or commit to deploy required: true type: string default: main @@ -13,11 +13,7 @@ on: required: true default: mpl-agent-identity type: choice - options: - - mpl-agent-identity - - mpl-agent-reputation - - mpl-agent-validation - - mpl-agent-tools + options: [mpl-agent-identity, mpl-agent-reputation, mpl-agent-validation, mpl-agent-tools] cluster: description: Cluster environment required: true @@ -37,304 +33,157 @@ on: type: boolean default: false -env: - CACHE: true +permissions: {} + +concurrency: + # One deploy per program and cluster at a time; a running deploy is never cancelled. + group: deploy-${{ inputs.program }}-${{ inputs.cluster }} + cancel-in-progress: false jobs: check_tag: - name: 'Check tag' + name: Check tag runs-on: ubuntu-latest + permissions: + # Confirms the release tag exists and resolves the ref to a commit. + contents: read outputs: - program: ${{ steps.set_program.outputs.program }} - type: ${{ steps.set_program.outputs.type }} + type: ${{ steps.set_type.outputs.type }} + sha: ${{ steps.set_type.outputs.sha }} steps: - - name: Check tag - id: set_program - env: - GIT_REF: ${{ inputs.git_ref }} - PROGRAM: ${{ inputs.program }} - run: | - echo "program=${PROGRAM}" >> $GITHUB_OUTPUT - # Require literal `release/@<...>` — `@*` regex would match `release/` with no version. - if [[ "${GIT_REF}" == "release/${PROGRAM}@"* ]]; then - echo "type=release" >> $GITHUB_OUTPUT - else - echo "type=ref" >> $GITHUB_OUTPUT - fi + - name: Check tag + id: set_type + env: + GIT_REF: ${{ inputs.git_ref }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PROGRAM: ${{ inputs.program }} + run: | + # A release tag deploys the asset attached to the GitHub release; any + # other ref is built and tested first. + case "$PROGRAM" in + mpl-agent-identity) TAG=mpl-agent-identity ;; + mpl-agent-reputation) TAG=mpl-agent-reputation ;; + mpl-agent-validation) TAG=mpl-agent-validation ;; + mpl-agent-tools) TAG=mpl-agent-tools ;; + *) echo "::error::unknown program '$PROGRAM'"; exit 1 ;; + esac + if [[ "$GIT_REF" == release/*@* ]]; then + if [[ "$GIT_REF" != "release/${TAG}@"* ]]; then + echo "::error::'$GIT_REF' is not a release tag of $PROGRAM (expected release/${TAG}@)" + exit 1 + fi + TYPE=release + else + TYPE=ref + fi + if [ "$TYPE" = release ]; then + # Only an existing tag skips build and test; a branch named like one does not. + OBJ="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${GIT_REF}" --jq '.object.type + " " + .object.sha')" \ + || { echo "::error::'$GIT_REF' is not an existing tag of this repository"; exit 1; } + read -r OBJ_TYPE SHA <<< "$OBJ" + if [ "$OBJ_TYPE" = tag ]; then + # Annotated tag: follow it to the commit. + SHA="$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${SHA}" --jq .object.sha)" + fi + elif [ -z "$GIT_REF" ]; then + SHA="$GITHUB_SHA" + else + SHA="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GIT_REF}" --jq .sha)" \ + || { echo "::error::'$GIT_REF' is not a commit, branch or tag of this repository"; exit 1; } + fi + echo "type=$TYPE" >> "$GITHUB_OUTPUT" + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "$GIT_REF -> $SHA ($TYPE)" + build_programs: name: Programs - uses: ./.github/workflows/build-programs.yml - secrets: inherit - needs: check_tag if: needs.check_tag.outputs.type == 'ref' + needs: check_tag + uses: metaplex-foundation/actions/.github/workflows/build-programs.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read with: - git_ref: ${{ inputs.git_ref }} + git_ref: ${{ needs.check_tag.outputs.sha }} + apt_packages: protobuf-compiler test_programs: - name: Test Programs - uses: ./.github/workflows/test-programs.yml - secrets: inherit - needs: [build_programs, check_tag] + name: Programs if: needs.check_tag.outputs.type == 'ref' + needs: [check_tag, build_programs] + uses: metaplex-foundation/actions/.github/workflows/test-programs.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read with: program_matrix: '["${{ inputs.program }}"]' - git_ref: ${{ inputs.git_ref }} + git_ref: ${{ needs.check_tag.outputs.sha }} + apt_packages: protobuf-compiler test_js: name: JS client - needs: [build_programs, check_tag] - uses: ./.github/workflows/test-js-client.yml - secrets: inherit if: needs.check_tag.outputs.type == 'ref' + needs: [check_tag, build_programs] + uses: metaplex-foundation/actions/.github/workflows/test-js-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read with: - git_ref: ${{ inputs.git_ref }} + git_ref: ${{ needs.check_tag.outputs.sha }} + node_matrix: '["22.x", "24.x"]' test_rust: name: Rust client - needs: [build_programs, check_tag] - uses: ./.github/workflows/test-rust-client.yml - secrets: inherit if: needs.check_tag.outputs.type == 'ref' + needs: [check_tag, build_programs] + uses: metaplex-foundation/actions/.github/workflows/test-rust-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read with: - git_ref: ${{ inputs.git_ref }} + git_ref: ${{ needs.check_tag.outputs.sha }} + apt_packages: protobuf-compiler + client_dir: clients - deploy_program: - name: Program / Deploy - runs-on: ubuntu-latest-16-cores - needs: [check_tag, test_js, test_programs, test_rust] + deploy: + name: Program if: | - always() - && (needs.test_js.result == 'success' || needs.test_js.result == 'skipped') - && (needs.test_programs.result == 'success' || needs.test_programs.result == 'skipped') - && (needs.test_rust.result == 'success' || needs.test_rust.result == 'skipped') + !cancelled() && !failure() + && needs.check_tag.result == 'success' + && (needs.check_tag.outputs.type == 'release' + || (needs.build_programs.result == 'success' + && needs.test_js.result == 'success' + && needs.test_programs.result == 'success' + && needs.test_rust.result == 'success')) + needs: [check_tag, build_programs, test_js, test_programs, test_rust] + uses: metaplex-foundation/actions/.github/workflows/deploy-program.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 permissions: contents: write - steps: - - name: Git checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.SVC_TOKEN }} - ref: ${{ inputs.git_ref }} - - - name: Resolve checked-out commit - id: resolved - run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Install Rust - uses: metaplex-foundation/actions/install-rust@v1 - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Install Solana - uses: metaplex-foundation/actions/install-solana@v1 - with: - version: ${{ env.DEPLOY_SOLANA_VERSION }} - cache: ${{ env.CACHE }} - - - name: Install cargo-release - uses: metaplex-foundation/actions/install-cargo-release@v1 - if: github.event.inputs.publish_crate == 'true' - with: - cache: ${{ env.CACHE }} - - - name: Set RPC - run: | - # We do this if waterfall because github actions does not allow dynamic access to secrets - if [ "${{ inputs.cluster }}" == "devnet" ]; then - echo RPC=${{ secrets.DEVNET_RPC }} >> $GITHUB_ENV - elif [ "${{ inputs.cluster }}" == "mainnet-beta" ]; then - echo RPC=${{ secrets.MAINNET_RPC }} >> $GITHUB_ENV - elif [ "${{ inputs.cluster }}" == "sonic-devnet" ]; then - echo RPC=${{ secrets.SONIC_DEVNET_RPC }} >> $GITHUB_ENV - elif [ "${{ inputs.cluster }}" == "sonic-testnet" ]; then - echo RPC=${{ secrets.SONIC_TESTNET_RPC }} >> $GITHUB_ENV - elif [ "${{ inputs.cluster }}" == "eclipse-devnet" ]; then - echo RPC=${{ secrets.ECLIPSE_DEVNET_RPC }} >> $GITHUB_ENV - elif [ "${{ inputs.cluster }}" == "eclipse-testnet" ]; then - echo RPC=${{ secrets.ECLIPSE_TESTNET_RPC }} >> $GITHUB_ENV - elif [ "${{ inputs.cluster }}" == "eclipse-mainnet" ]; then - echo RPC=${{ secrets.ECLIPSE_MAINNET_RPC }} >> $GITHUB_ENV - fi - - - name: Identify Program - env: - PROGRAM: ${{ inputs.program }} - CLUSTER: ${{ inputs.cluster }} - IDENTITY_PROGRAM_ID: ${{ secrets.IDENTITY_PROGRAM_ID }} - REPUTATION_PROGRAM_ID: ${{ secrets.REPUTATION_PROGRAM_ID }} - VALIDATION_PROGRAM_ID: ${{ secrets.VALIDATION_PROGRAM_ID }} - TOOLS_PROGRAM_ID: ${{ secrets.TOOLS_PROGRAM_ID }} - PROGRAM_SOLANA_DEPLOY_KEY: ${{ secrets.PROGRAM_SOLANA_DEPLOY_KEY }} - PROGRAM_SONIC_DEPLOY_KEY: ${{ secrets.PROGRAM_SONIC_DEPLOY_KEY }} - PROGRAM_ECLIPSE_DEPLOY_KEY: ${{ secrets.PROGRAM_ECLIPSE_DEPLOY_KEY }} - run: | - PROGRAM_NAME=$(echo "${PROGRAM}" | tr - _) - echo PROGRAM_NAME="${PROGRAM_NAME}" >> $GITHUB_ENV - - # Per-program program-id keypair secret (waterfall — github actions does not allow dynamic access to secrets) - case "${PROGRAM}" in - mpl-agent-identity) PROGRAM_ID_KEYPAIR="${IDENTITY_PROGRAM_ID}" ;; - mpl-agent-reputation) PROGRAM_ID_KEYPAIR="${REPUTATION_PROGRAM_ID}" ;; - mpl-agent-validation) PROGRAM_ID_KEYPAIR="${VALIDATION_PROGRAM_ID}" ;; - mpl-agent-tools) PROGRAM_ID_KEYPAIR="${TOOLS_PROGRAM_ID}" ;; - *) echo "Invalid program: ${PROGRAM}"; exit 1 ;; - esac - - if [ -z "${PROGRAM_ID_KEYPAIR}" ]; then - echo "Missing program-id secret for ${PROGRAM} (set IDENTITY_PROGRAM_ID / REPUTATION_PROGRAM_ID / VALIDATION_PROGRAM_ID / TOOLS_PROGRAM_ID)." - exit 1 - fi - echo "${PROGRAM_ID_KEYPAIR}" > ./program-id.json - - case "${CLUSTER}" in - sonic*) DEPLOYER_KEY="${PROGRAM_SONIC_DEPLOY_KEY}" ;; - eclipse*) DEPLOYER_KEY="${PROGRAM_ECLIPSE_DEPLOY_KEY}" ;; - devnet) DEPLOYER_KEY="${PROGRAM_SOLANA_DEPLOY_KEY}" ;; - mainnet-beta) DEPLOYER_KEY="${PROGRAM_SOLANA_DEPLOY_KEY}" ;; - *) echo "Invalid cluster: ${CLUSTER}"; exit 1 ;; - esac - - if [ -z "${DEPLOYER_KEY}" ]; then - echo "Missing deployer-key secret for cluster ${CLUSTER}." - exit 1 - fi - echo "${DEPLOYER_KEY}" > ./deployer-key.json - echo DEPLOY_TYPE="direct" >> $GITHUB_ENV - - - name: Sanitize Ref - id: sanitize - shell: bash - run: | - REF="${{ inputs.git_ref }}" - if [ -z "$REF" ]; then - REF="default" - fi - SANITIZED=${REF//\//-} - echo "sanitized=$SANITIZED" >> "$GITHUB_OUTPUT" - - - name: Download program builds - uses: actions/download-artifact@v4 - if: needs.check_tag.outputs.type == 'ref' - with: - name: program-builds-${{ steps.sanitize.outputs.sanitized }} - - - name: Download release asset - uses: actions/github-script@v5 - id: get_release - if: needs.check_tag.outputs.type == 'release' - with: - script: | - const tag = "${{ inputs.git_ref }}"; - const assetName = "${{ env.PROGRAM_NAME }}_program.so"; - - // Fetch the release associated with the tag - const release = await github.rest.repos.getReleaseByTag({ - owner: context.repo.owner, - repo: context.repo.repo, - tag: tag - }); - - if (release.status !== 200) { - throw new Error(`Failed to fetch release for tag ${tag}`); - } - - const asset = release.data.assets.find(asset => asset.name === assetName); - if (!asset) { - throw new Error(`Asset ${assetName} not found in release tagged ${tag}`); - } - - core.setOutput("url", asset.url); - - - name: Download the Selected Asset - if: needs.check_tag.outputs.type == 'release' - run: | - mkdir -p ${{ github.workspace }}/programs/.bin - curl -L -o ${{ github.workspace }}/programs/.bin/${{ env.PROGRAM_NAME }}_program.so \ - -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ - -H "Accept: application/octet-stream" \ - "${{ steps.get_release.outputs.url }}" - - - name: Deploy Program - if: github.event.inputs.dry_run == 'false' && env.DEPLOY_TYPE == 'direct' - run: | - echo "Deploying ${{ needs.check_tag.outputs.program }} to ${{ inputs.cluster }}" - - solana -v program deploy ./programs/.bin/${{ env.PROGRAM_NAME }}_program.so \ - -u ${{ env.RPC }} \ - --program-id ./program-id.json \ - -k ./deployer-key.json \ - --max-sign-attempts 100 \ - --use-rpc - - rm ./deployer-key.json - rm ./program-id.json - - - name: Deploy squads buffer - if: github.event.inputs.dry_run == 'false' && env.DEPLOY_TYPE == 'squads' - run: | - echo "Deploying buffer for ${{ inputs.program }} on ${{ inputs.cluster }}" - echo ${{ secrets.SQUADS_BOT_KEY }} > ./submitter-key.json - - BUFFER=$(solana program write-buffer -u ${{ env.RPC }} -k ./submitter-key.json --max-sign-attempts 100 --use-rpc ./programs/.bin/${{ env.PROGRAM_NAME }}_program.so | awk '{print $2}') - echo "Buffer: $BUFFER" - - solana program set-buffer-authority $BUFFER \ - --new-buffer-authority ${{ env.SQUADS_VAULT }} \ - -k ./submitter-key.json \ - -u ${{ env.RPC }} - - rm ./submitter-key.json - - echo "BUFFER=$BUFFER" >> $GITHUB_ENV - - - - name: Create env tag - uses: actions/github-script@v7 - if: github.event.inputs.dry_run == 'false' - env: - REF_TYPE: ${{ needs.check_tag.outputs.type }} - GIT_REF: ${{ inputs.git_ref }} - PROGRAM: ${{ needs.check_tag.outputs.program }} - CLUSTER: ${{ inputs.cluster }} - RESOLVED_SHA: ${{ steps.resolved.outputs.sha }} - with: - github-token: ${{ secrets.GH_TAGGER_TOKEN }} - script: | - // For release tags we can dereference via the GitHub API. For ref inputs - // (branch names, SHAs) getRef won't accept a bare value, so use the SHA - // we resolved from the checked-out workspace instead. - let sha; - if (process.env.REF_TYPE === 'release') { - const refData = await github.rest.git.getRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: `tags/${process.env.GIT_REF}`, - }); - sha = refData.data.object.sha; - } else { - sha = process.env.RESOLVED_SHA; - if (!sha) { - throw new Error('Resolved checkout SHA is empty'); - } - } - - const envRef = `${process.env.PROGRAM}-${process.env.CLUSTER}`; - try { - await github.rest.git.createRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: `refs/tags/${envRef}`, - sha, - }); - } catch (err) { - if (err.status !== 422) throw err; - await github.rest.git.updateRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: `tags/${envRef}`, - sha, - }); - } + # GitHub Actions cannot look secrets up by a computed name, so the + # per-cluster secrets are mapped onto the shared workflow's fixed names here. + secrets: + RPC_URL: >- + ${{ inputs.cluster == 'devnet' && secrets.DEVNET_RPC + || inputs.cluster == 'mainnet-beta' && secrets.MAINNET_RPC + || inputs.cluster == 'sonic-devnet' && secrets.SONIC_DEVNET_RPC + || inputs.cluster == 'sonic-testnet' && secrets.SONIC_TESTNET_RPC + || inputs.cluster == 'eclipse-devnet' && secrets.ECLIPSE_DEVNET_RPC + || inputs.cluster == 'eclipse-testnet' && secrets.ECLIPSE_TESTNET_RPC + || inputs.cluster == 'eclipse-mainnet' && secrets.ECLIPSE_MAINNET_RPC + || '' }} + PROGRAM_ID_KEYPAIR: >- + ${{ inputs.program == 'mpl-agent-identity' && secrets.IDENTITY_PROGRAM_ID + || inputs.program == 'mpl-agent-reputation' && secrets.REPUTATION_PROGRAM_ID + || inputs.program == 'mpl-agent-validation' && secrets.VALIDATION_PROGRAM_ID + || inputs.program == 'mpl-agent-tools' && secrets.TOOLS_PROGRAM_ID + || '' }} + DEPLOYER_KEYPAIR: >- + ${{ inputs.cluster == 'devnet' && secrets.PROGRAM_SOLANA_DEPLOY_KEY + || inputs.cluster == 'mainnet-beta' && secrets.PROGRAM_SOLANA_DEPLOY_KEY + || startsWith(inputs.cluster, 'sonic') && secrets.PROGRAM_SONIC_DEPLOY_KEY + || startsWith(inputs.cluster, 'eclipse') && secrets.PROGRAM_ECLIPSE_DEPLOY_KEY + || '' }} + SQUADS_BOT_KEY: ${{ secrets.SQUADS_BOT_KEY }} + with: + program: ${{ inputs.program }} + cluster: ${{ inputs.cluster }} + git_ref: ${{ needs.check_tag.outputs.sha }} + release_tag: ${{ needs.check_tag.outputs.type == 'release' && inputs.git_ref || '' }} + dry_run: ${{ inputs.dry_run }} + runner: ubuntu-latest-16-cores diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7a7c25..6a5f029 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,139 +1,83 @@ name: Main +# Thin orchestrator over the shared workflows in metaplex-foundation/actions. +# Repository facts live in .github/.env (toolchain pins), .github/programs.json +# (program descriptors) and .github/file-filters.yml (change detection). + on: push: branches: [main] pull_request: branches: [main] -env: - CACHE: true +permissions: {} jobs: changes: name: Detect changes - runs-on: ubuntu-latest - outputs: - any: ${{ steps.changes.outputs.any }} - programs: ${{ steps.changes.outputs.programs }} - program_matrix: ${{ steps.program_matrix.outputs.matrix }} - js_client: ${{ steps.changes.outputs.js_client }} - rust_client: ${{ steps.changes.outputs.rust_client }} - steps: - - name: Git checkout - uses: actions/checkout@v4 - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Detect changes - uses: dorny/paths-filter@v2 - id: changes - with: - filters: .github/file-filters.yml - - - name: Filter program matrix - id: program_matrix - uses: metaplex-foundation/actions/filter-matrix@v1 - with: - matrix: ${{ env.PROGRAMS }} - changes: ${{ steps.changes.outputs.changes }} - suffix: _program + uses: metaplex-foundation/actions/.github/workflows/detect-changes.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + pull-requests: read build_programs: name: Programs - if: ${{ needs.changes.outputs.any == 'true' }} + if: needs.changes.outputs.any == 'true' needs: changes - uses: ./.github/workflows/build-programs.yml - secrets: inherit + uses: metaplex-foundation/actions/.github/workflows/build-programs.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + with: + apt_packages: protobuf-compiler test_programs: name: Programs - if: ${{ needs.changes.outputs.programs == 'true' }} + if: needs.changes.outputs.programs == 'true' needs: changes - uses: ./.github/workflows/test-programs.yml - secrets: inherit + uses: metaplex-foundation/actions/.github/workflows/test-programs.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read with: program_matrix: ${{ needs.changes.outputs.program_matrix }} + apt_packages: protobuf-compiler generate_clients: name: Generate clients - if: ${{ needs.changes.outputs.any == 'true' }} - needs: build_programs - runs-on: ubuntu-latest - steps: - - name: Git checkout - uses: actions/checkout@v4 - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Install Linux Build Deps - run: sudo apt-get update && sudo apt-get install -y pkg-config build-essential libudev-dev - - - name: Install Rust - uses: metaplex-foundation/actions/install-rust@v1 - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Install Solana - uses: metaplex-foundation/actions/install-solana@v1 - with: - version: ${{ env.SOLANA_VERSION }} - cache: ${{ env.CACHE }} - - - name: Cache program dependencies - if: env.CACHE == 'true' - uses: metaplex-foundation/actions/cache-programs@v1 - - - name: Install Node.js - uses: metaplex-foundation/actions/install-node-with-pnpm@v1 - with: - version: ${{ env.NODE_VERSION }} - cache: ${{ env.CACHE }} - dependencies: true - - - name: Cache IDL generators - if: env.CACHE == 'true' - uses: metaplex-foundation/actions/cache-idl-generators@v1 - - - name: Generate IDLs and clients - run: pnpm generate - - - name: Ensure working directory is clean - run: test -z "$(git status --porcelain)" + if: needs.changes.outputs.any == 'true' + needs: [changes, build_programs] + uses: metaplex-foundation/actions/.github/workflows/generate-clients.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read test_js: - if: needs.changes.outputs.js_client == 'true' name: JS Client - needs: generate_clients - uses: ./.github/workflows/test-js-client.yml - secrets: inherit - - # benchmark: - # if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - # name: Benchmark - # needs: generate_clients - # uses: ./.github/workflows/benchmark.yml - # secrets: inherit - - # benchmark_summary: - # if: ${{ github.event_name == 'pull_request' }} - # name: Benchmark Summary - # needs: generate_clients - # uses: ./.github/workflows/benchmark-summary.yml - # secrets: inherit + if: needs.changes.outputs.js_client == 'true' + needs: [changes, generate_clients] + uses: metaplex-foundation/actions/.github/workflows/test-js-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + with: + node_matrix: '["22.x", "24.x"]' build_rust_client: - if: needs.changes.outputs.rust_client == 'true' name: Rust Client - needs: generate_clients - uses: ./.github/workflows/build-rust-client.yml - secrets: inherit + if: needs.changes.outputs.rust_client == 'true' + needs: [changes, generate_clients] + uses: metaplex-foundation/actions/.github/workflows/build-rust-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + with: + apt_packages: protobuf-compiler + client_matrix: '["clients/rust-identity", "clients/rust-reputation", "clients/rust-validation", "clients/rust-tools"]' + features: --all-features test_rust_client: - if: needs.changes.outputs.rust_client == 'true' name: Rust Client - needs: generate_clients - uses: ./.github/workflows/test-rust-client.yml - secrets: inherit + if: needs.changes.outputs.rust_client == 'true' + needs: [changes, generate_clients] + uses: metaplex-foundation/actions/.github/workflows/test-rust-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + with: + apt_packages: protobuf-compiler + client_dir: clients diff --git a/.github/workflows/publish-js-client.yml b/.github/workflows/publish-js-client.yml index 4b4c7b1..5804d34 100644 --- a/.github/workflows/publish-js-client.yml +++ b/.github/workflows/publish-js-client.yml @@ -3,138 +3,120 @@ name: Publish JS Client on: workflow_dispatch: inputs: + git_ref: + description: Release tag (release/mpl-agent-identity@0.1.0) or commit to publish from + required: false + type: string + default: '' bump: description: Version bump required: true default: patch type: choice - options: - - patch - - minor - - major - - prerelease - - prepatch - - preminor - - premajor + options: [patch, minor, major, prerelease, prepatch, preminor, premajor] tag: - description: NPM Tag (and preid for pre-releases) + description: NPM tag (and preid for pre-releases) required: true type: string default: latest create_release: - description: Create a GitHub release + description: Open the release pull request and deploy docs required: true type: boolean default: true -env: - CACHE: true +permissions: {} + +concurrency: + # One JS publish at a time: each bumps the version and opens a release PR. + group: publish-js-client + cancel-in-progress: false jobs: + check_tag: + name: Check tag + runs-on: ubuntu-latest + permissions: + # Confirms the release tag exists and resolves the ref to a commit. + contents: read + outputs: + type: ${{ steps.set_type.outputs.type }} + sha: ${{ steps.set_type.outputs.sha }} + steps: + - name: Check tag + id: set_type + env: + GIT_REF: ${{ inputs.git_ref }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # A program release tag was already built and tested when it was cut. + case "$GIT_REF" in + release/mpl-agent-identity@*|release/mpl-agent-reputation@*|release/mpl-agent-validation@*|release/mpl-agent-tools@*) TYPE=release ;; + release/*@*) echo "::error::'$GIT_REF' is not a release tag of this repository (expected release/mpl-agent-identity@, release/mpl-agent-reputation@, release/mpl-agent-validation@, release/mpl-agent-tools@)"; exit 1 ;; + *) TYPE=ref ;; + esac + if [ "$TYPE" = release ]; then + # Only an existing tag skips build and test; a branch named like one does not. + OBJ="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${GIT_REF}" --jq '.object.type + " " + .object.sha')" \ + || { echo "::error::'$GIT_REF' is not an existing tag of this repository"; exit 1; } + read -r OBJ_TYPE SHA <<< "$OBJ" + if [ "$OBJ_TYPE" = tag ]; then + # Annotated tag: follow it to the commit. + SHA="$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${SHA}" --jq .object.sha)" + fi + elif [ -z "$GIT_REF" ]; then + SHA="$GITHUB_SHA" + else + SHA="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GIT_REF}" --jq .sha)" \ + || { echo "::error::'$GIT_REF' is not a commit, branch or tag of this repository"; exit 1; } + fi + echo "type=$TYPE" >> "$GITHUB_OUTPUT" + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "$GIT_REF -> $SHA ($TYPE)" + build_programs: name: Programs - uses: ./.github/workflows/build-programs.yml - secrets: inherit + if: needs.check_tag.outputs.type == 'ref' + needs: check_tag + uses: metaplex-foundation/actions/.github/workflows/build-programs.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + with: + git_ref: ${{ needs.check_tag.outputs.sha }} + apt_packages: protobuf-compiler test_js: name: JS client - needs: build_programs - uses: ./.github/workflows/test-js-client.yml - secrets: inherit + if: needs.check_tag.outputs.type == 'ref' + needs: [check_tag, build_programs] + uses: metaplex-foundation/actions/.github/workflows/test-js-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + with: + git_ref: ${{ needs.check_tag.outputs.sha }} + node_matrix: '["22.x", "24.x"]' - publish_js: - name: JS client / Publish - runs-on: ubuntu-latest - needs: test_js + publish: + name: JS client + if: | + !cancelled() && !failure() + && needs.check_tag.result == 'success' + && (needs.check_tag.outputs.type == 'release' + || (needs.build_programs.result == 'success' + && needs.test_js.result == 'success')) + needs: [check_tag, build_programs, test_js] + uses: metaplex-foundation/actions/.github/workflows/publish-js-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 permissions: contents: write - packages: write pull-requests: write id-token: write - steps: - - name: Git checkout - uses: actions/checkout@v4 - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 8.9.0 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 24.x - registry-url: 'https://registry.npmjs.org' - cache: 'pnpm' - - - name: Install dependencies - uses: metaplex-foundation/actions/install-node-dependencies@v1 - with: - folder: ./clients/js - cache: ${{ env.CACHE }} - key: clients-js - - - name: Build - working-directory: ./clients/js - run: pnpm build - - - name: Bump - id: bump - working-directory: ./clients/js - run: | - if [ "${{ startsWith(inputs.bump, 'pre') }}" == "true" ]; then - pnpm version ${{ inputs.bump }} --preid ${{ inputs.tag }} --no-git-tag-version - else - pnpm version ${{ inputs.bump }} --no-git-tag-version - fi - echo "new_version=$(pnpm pkg get version | sed 's/"//g')" >> $GITHUB_OUTPUT - - - name: Publish - working-directory: ./clients/js - run: npm publish --access public --tag ${{ inputs.tag }} --provenance - - - name: Create release pull request - if: ${{ github.event.inputs.create_release == 'true' }} - uses: peter-evans/create-pull-request@v6 - with: - commit-message: 'chore: release JS client v${{ steps.bump.outputs.new_version }}' - branch: release/js-client-v${{ steps.bump.outputs.new_version }} - title: 'chore: release JS client v${{ steps.bump.outputs.new_version }}' - body: | - ## Summary - - publish-js-client workflow bumped the package to `${{ steps.bump.outputs.new_version }}` - - once merged, run the release workflow to tag `js@v${{ steps.bump.outputs.new_version }}` - labels: release - - - name: Create GitHub release - if: github.event.inputs.create_release == 'true' - uses: ncipollo/release-action@v1 - with: - tag: js@v${{ steps.bump.outputs.new_version }} - - deploy_js_docs: - name: JS client / Deploy docs - runs-on: ubuntu-latest - needs: publish_js - environment: - name: js-client-documentation - url: ${{ steps.deploy.outputs.url }} - steps: - - name: Git checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Deploy to Vercel - id: deploy - env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - working-directory: ./clients/js - run: echo "url=$(vercel deploy --prod --token=${{ secrets.VERCEL_TOKEN }})" | tee $GITHUB_OUTPUT + secrets: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + with: + git_ref: ${{ needs.check_tag.outputs.sha }} + bump: ${{ inputs.bump }} + tag: ${{ inputs.tag }} + create_release: ${{ inputs.create_release }} + deploy_docs: true diff --git a/.github/workflows/publish-rust-client.yml b/.github/workflows/publish-rust-client.yml index 5673301..a214993 100644 --- a/.github/workflows/publish-rust-client.yml +++ b/.github/workflows/publish-rust-client.yml @@ -1,5 +1,7 @@ name: Publish Rust Client +run-name: Publish Rust Client (${{ inputs.level }}${{ inputs.dry_run && ', dry run' || '' }}) + on: workflow_dispatch: inputs: @@ -7,27 +9,15 @@ on: description: Client to publish required: true type: choice - options: - - rust-identity - - rust-reputation - - rust-validation - - rust-tools + options: [rust-identity, rust-reputation, rust-validation, rust-tools] level: description: Level required: true default: patch type: choice - options: - - patch - - minor - - major - - rc - - beta - - alpha - - release - - version + options: [patch, minor, major, rc, beta, alpha, release, version] version: - description: Version + description: Version (when level is `version`) required: false type: string dry_run: @@ -36,81 +26,53 @@ on: default: true type: boolean -env: - CACHE: true +permissions: {} + +concurrency: + # One Rust publish per crate at a time. + group: publish-rust-client-${{ inputs.client }} + cancel-in-progress: false jobs: build_programs: name: Programs - uses: ./.github/workflows/build-programs.yml - secrets: inherit + uses: metaplex-foundation/actions/.github/workflows/build-programs.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + with: + apt_packages: protobuf-compiler build_rust_client: - name: Rust Client - uses: ./.github/workflows/build-rust-client.yml - secrets: inherit + name: Rust client + uses: metaplex-foundation/actions/.github/workflows/build-rust-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + with: + apt_packages: protobuf-compiler + client_matrix: '["clients/rust-identity", "clients/rust-reputation", "clients/rust-validation", "clients/rust-tools"]' + features: --all-features test_rust_client: - name: Rust Client + name: Rust client needs: [build_programs, build_rust_client] - uses: ./.github/workflows/test-rust-client.yml - secrets: inherit + uses: metaplex-foundation/actions/.github/workflows/test-rust-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: read + with: + apt_packages: protobuf-compiler + client_dir: clients - publish_crate: - name: Rust Client / Publish Crate - runs-on: ubuntu-latest + publish: + name: Rust client needs: test_rust_client + uses: metaplex-foundation/actions/.github/workflows/publish-rust-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 permissions: contents: write - steps: - - name: Git checkout - uses: actions/checkout@v4 - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Install Rust - uses: metaplex-foundation/actions/install-rust@v1 - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Install cargo-release - uses: metaplex-foundation/actions/install-cargo-release@v1 - with: - cache: ${{ env.CACHE }} - - - name: Publish crate - working-directory: ./clients/${{ inputs.client }} - run: | - if [ "${{ inputs.level }}" == "version" ]; then - BUMP=${{ inputs.version }} - else - BUMP=${{ inputs.level }} - fi - - if [ "${{ inputs.dry_run }}" == "false" ]; then - OPTIONS="--no-push --no-tag --no-confirm --execute" - git config user.name ${{ env.COMMIT_USER_NAME }} - git config user.email ${{ env.COMMIT_USER_EMAIL }} - fi - - echo "Cargo logging in" - cargo login ${{ secrets.CRATES_TOKEN }} - echo "Cargo release" - cargo release $BUMP $OPTIONS - - if [ "${{ inputs.dry_run }}" == "false" ]; then - git reset --soft HEAD~1 - fi - - CLIENT_NAME=`grep -E '^name\s*=' Cargo.toml | awk -F '"' '{print $2}'` - CLIENT_VERSION=`grep -E '^version\s*=' Cargo.toml | awk -F '"' '{print $2}'` - echo CLIENT_NAME="${CLIENT_NAME}" >> $GITHUB_ENV - echo CLIENT_VERSION="${CLIENT_VERSION}" >> $GITHUB_ENV - - - name: Commit and tag new version - uses: stefanzweifel/git-auto-commit-action@v4 - if: github.event.inputs.dry_run == 'false' - with: - commit_message: "chore: Release ${{ env.CLIENT_NAME }} version ${{ env.CLIENT_VERSION }}" - tagging_message: ${{ env.CLIENT_NAME }}@v${{ env.CLIENT_VERSION }} + pull-requests: write + secrets: + CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + with: + level: ${{ inputs.level }} + version: ${{ inputs.version }} + dry_run: ${{ inputs.dry_run }} + client_dir: ${{ inputs.client == 'rust-identity' && 'clients/rust-identity' || inputs.client == 'rust-reputation' && 'clients/rust-reputation' || inputs.client == 'rust-validation' && 'clients/rust-validation' || inputs.client == 'rust-tools' && 'clients/rust-tools' || '' }} diff --git a/.github/workflows/release-js-client.yml b/.github/workflows/release-js-client.yml new file mode 100644 index 0000000..8da0ce1 --- /dev/null +++ b/.github/workflows/release-js-client.yml @@ -0,0 +1,25 @@ +name: Release JS Client + +# Tags and releases the JS client once the pull request opened by +# publish-js-client is merged. + +on: + pull_request: + types: [closed] + branches: [main] + +permissions: {} + +jobs: + release: + name: JS client + if: | + github.event.pull_request.merged == true + && startsWith(github.event.pull_request.head.ref, 'release/js-client-') + && github.event.pull_request.head.repo.full_name == github.repository + uses: metaplex-foundation/actions/.github/workflows/release-js-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: write + with: + merge_commit_sha: ${{ github.event.pull_request.merge_commit_sha }} + head_ref: ${{ github.event.pull_request.head.ref }} diff --git a/.github/workflows/release-rust-client.yml b/.github/workflows/release-rust-client.yml new file mode 100644 index 0000000..64fed83 --- /dev/null +++ b/.github/workflows/release-rust-client.yml @@ -0,0 +1,26 @@ +name: Release Rust Client + +# Tags and releases the Rust client once the pull request opened by +# publish-rust-client is merged. + +on: + pull_request: + types: [closed] + branches: [main] + +permissions: {} + +jobs: + release: + name: Rust client + if: | + github.event.pull_request.merged == true + && startsWith(github.event.pull_request.head.ref, 'release/rust-client-') + && github.event.pull_request.head.repo.full_name == github.repository + uses: metaplex-foundation/actions/.github/workflows/release-rust-client.yml@8fcadb6df757cba34aa8475373886eca9b6ce8cf # v2.0.1 + permissions: + contents: write + with: + merge_commit_sha: ${{ github.event.pull_request.merge_commit_sha }} + client_matrix: '["clients/rust-identity", "clients/rust-reputation", "clients/rust-validation", "clients/rust-tools"]' + head_ref: ${{ github.event.pull_request.head.ref }} diff --git a/.github/workflows/test-js-client.yml b/.github/workflows/test-js-client.yml deleted file mode 100644 index 0efda83..0000000 --- a/.github/workflows/test-js-client.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Test JS client - -on: - workflow_call: - inputs: - git_ref: - type: string - -env: - CACHE: true - -jobs: - test: - name: Test - runs-on: ubuntu-latest - strategy: - matrix: - node: ["22.x", "24.x"] - steps: - - name: Git checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.git_ref }} - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Sanitize Ref - id: sanitize - shell: bash - run: | - REF="${{ inputs.git_ref }}" - if [ -z "$REF" ]; then - REF="default" - fi - SANITIZED=${REF//\//-} - echo "sanitized=$SANITIZED" >> "$GITHUB_OUTPUT" - - - name: Start validator - uses: metaplex-foundation/actions/start-validator@v1 - with: - node: ${{ matrix.node }} - solana: ${{ env.SOLANA_VERSION }} - cache: ${{ env.CACHE }} - artifacts: program-builds-${{ steps.sanitize.outputs.sanitized }} - - - name: Install dependencies - uses: metaplex-foundation/actions/install-node-dependencies@v1 - with: - folder: ./clients/js - cache: ${{ env.CACHE }} - key: clients-js - - - name: Build - working-directory: ./clients/js - run: pnpm build - - - name: Test - working-directory: ./clients/js - run: pnpm test - - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Git checkout - uses: actions/checkout@v4 - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Install Node.js - uses: metaplex-foundation/actions/install-node-with-pnpm@v1 - with: - version: ${{ env.NODE_VERSION }} - cache: ${{ env.CACHE }} - - - name: Install dependencies - uses: metaplex-foundation/actions/install-node-dependencies@v1 - with: - folder: ./clients/js - cache: ${{ env.CACHE }} - key: clients-js - - - name: Format - working-directory: ./clients/js - run: pnpm format - - - name: Lint - working-directory: ./clients/js - run: pnpm lint diff --git a/.github/workflows/test-programs.yml b/.github/workflows/test-programs.yml deleted file mode 100644 index fadccac..0000000 --- a/.github/workflows/test-programs.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Test Programs - -on: - workflow_call: - inputs: - program_matrix: - type: string - git_ref: - type: string - -env: - CACHE: true - -jobs: - test_programs: - name: Test - runs-on: ubuntu-latest - strategy: - matrix: - program: ${{ fromJson(inputs.program_matrix) }} - steps: - - name: Git checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.git_ref }} - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Install Rust - uses: metaplex-foundation/actions/install-rust@v1 - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Install Solana - uses: metaplex-foundation/actions/install-solana@v1 - with: - version: ${{ env.SOLANA_VERSION }} - cache: ${{ env.CACHE }} - - - name: Install Protobuf Compiler - run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - - - name: Cache program dependencies - if: env.CACHE == 'true' - uses: metaplex-foundation/actions/cache-program@v1 - with: - folder: ./programs/${{ matrix.program }} - key: program-${{ matrix.program }} - - - name: Run cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all --manifest-path ./programs/${{ matrix.program }}/Cargo.toml -- --check - - - name: Run cargo clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-targets --all-features --no-deps --manifest-path ./programs/${{ matrix.program }}/Cargo.toml - - - name: Run tests - shell: bash - working-directory: configs/scripts/program - run: RUST_LOG=error ./test.sh - env: - PROGRAM: ${{ matrix.program }} diff --git a/.github/workflows/test-rust-client.yml b/.github/workflows/test-rust-client.yml deleted file mode 100644 index 5fd341c..0000000 --- a/.github/workflows/test-rust-client.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Test Rust Client - -on: - workflow_call: - inputs: - program_matrix: - type: string - git_ref: - type: string - -env: - CACHE: true - -jobs: - test_sdk: - name: Test - runs-on: ubuntu-latest - steps: - - name: Git checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.git_ref }} - - - name: Load environment variables - run: cat .github/.env >> $GITHUB_ENV - - - name: Install Rust - uses: metaplex-foundation/actions/install-rust@v1 - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Install Solana - uses: metaplex-foundation/actions/install-solana@v1 - with: - version: ${{ env.SOLANA_VERSION }} - cache: ${{ env.CACHE }} - - - name: Install Protobuf Compiler - run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - - - name: Cache Rust client test dependencies - uses: metaplex-foundation/actions/cache-crate@v1 - with: - folder: ./clients - key: rust-clients-test - - - name: Sanitize Ref - id: sanitize - shell: bash - run: | - REF="${{ inputs.git_ref }}" - if [ -z "$REF" ]; then - REF="default" - fi - SANITIZED=${REF//\//-} - echo "sanitized=$SANITIZED" >> "$GITHUB_OUTPUT" - - name: Download program builds - uses: actions/download-artifact@v4 - with: - name: program-builds-${{ steps.sanitize.outputs.sanitized }} - - - name: Run tests - shell: bash - working-directory: configs/scripts/client - run: RUST_LOG=error ./test-rust.sh \ No newline at end of file diff --git a/clients/js/CONTRIBUTING.md b/clients/js/CONTRIBUTING.md index 260a652..dcd5929 100644 --- a/clients/js/CONTRIBUTING.md +++ b/clients/js/CONTRIBUTING.md @@ -40,9 +40,21 @@ For this to work, some initial setup is required on the repository as explained ## Setting up GitHub actions +The publish workflow authenticates to NPM with [trusted publishing](https://docs.npmjs.com/trusted-publishers) +rather than a long-lived token: it requests an OIDC token from GitHub (`id-token: write`) and +publishes with `npm` 11.5.1 or newer, so no `NPM_TOKEN` secret is read or needed. + +To enable it, on npmjs.com open the `@metaplex-foundation/mpl-agent-registry` package settings and add a GitHub Actions +trusted publisher for this repository with the workflow filename `publish-js-client.yml` (the +filename alone, not its path). Under allowed actions, permit direct publishing with `npm publish`: +a publisher created after September 2026 allows only `npm stage publish` by default, and the +workflow runs `npm publish`. The npm account performing that setup must be able to publish the +package, and the publisher must complete its first publish within two days of being created. +The release pull request the workflow opens afterwards needs "Allow GitHub Actions to create and +approve pull requests" enabled under the repository's Actions settings. + To publish JavaScript clients using GitHub actions, we first need the following secret variables to be set up on the repository. -- `NPM_TOKEN` — An access token that can publish your packages to NPM. - `VERCEL_TOKEN` — An access token that can deploy to Vercel. - `VERCEL_ORG_ID` — The ID of the Vercel organization you want to deploy to.