Packs

Org paved paths are capabilities (secret names only; values stay in GitHub):

Amber is the knob (zipxCapabilities += …). Each green pack is a paved Publish/docs capability that lands in its destination; you only name secrets in code, values stay in GitHub.

ZipxCentral

// Aggregate (preferred for libraries / dogfood)
zipxCapabilities += ZipxCentral.release   // GPG import + publishSigned; sonaRelease

// Graph escape hatch
zipxCapabilities ++= Seq(ZipxCentral.publishSigned, ZipxCentral.releaseOnce)
DocsRender.job("publish")(ZipxCentral.release)
publish:
  name: publish
  runs-on: ubuntu-latest
  if: startsWith(github.ref, 'refs/tags/v')
  env:
    PGP_KEY_HEX: ${{ secrets.PGP_KEY_HEX }}
    PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
    SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
    SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
  steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
      with:
        fetch-depth: "0"
        fetch-tags: "true"
    - name: Setup JDK 21
      uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
      with:
        distribution: temurin
        java-version: "21"
    - uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
      with:
        disk-cache: "false"
    - name: Cache sbt
      uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
      with:
        path: |
          ~/.sbt
          ~/.cache/sbt
          ~/.cache/coursier
          target
        key: ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-publish
        restore-keys: |
          ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-
          ubuntu-latest-jdk21-sbt-0.1.0-ci-
          ubuntu-latest-jdk21-sbt-0.1.0-
          ubuntu-latest-jdk21-sbt-
    - name: Import signing key
      run: |
        mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
        echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf
        echo "pinentry-mode loopback"   >> ~/.gnupg/gpg.conf
        gpgconf --kill gpg-agent || true
        echo "$PGP_SECRET" | base64 --decode | gpg --batch --import
      env:
        PGP_SECRET: ${{ secrets.PGP_SECRET }}
    - name: publish
      run: sbt 'publishSigned; sonaRelease'

ZipxGitHubPackages

zipxCapabilities ++= Seq(
  ZipxCentral.release,
  ZipxGitHubPackages.sameRepo(repository = Some("acme/my-fork")),
)
// Shared registry PAT: ZipxGitHubPackages.sharedRegistry(tokenSecret = "GH_PACKAGES_TOKEN")

Thin CI wiring (packages: write + token + PUBLISH_GITHUB_PACKAGES=true). sbt owns publishTo / Credentials. See Job conditions for fork gates and multi-publish recipes.

DocsRender.job("github-packages")(
  ZipxGitHubPackages.sameRepo(repository = Some("acme/fork"))
)
github-packages:
  name: github-packages
  runs-on: ubuntu-latest
  if: (startsWith(github.ref, 'refs/tags/v')) && (github.repository == 'acme/fork')
  permissions:
    contents: read
    packages: write
  env:
    GITHUB_TOKEN: ${{ github.token }}
    PUBLISH_GITHUB_PACKAGES: "true"
  steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
      with:
        fetch-depth: "0"
        fetch-tags: "true"
    - name: Setup JDK 21
      uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
      with:
        distribution: temurin
        java-version: "21"
    - uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
      with:
        disk-cache: "false"
    - name: Cache sbt
      uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
      with:
        path: |
          ~/.sbt
          ~/.cache/sbt
          ~/.cache/coursier
          target
        key: ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-github-packages
        restore-keys: |
          ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-
          ubuntu-latest-jdk21-sbt-0.1.0-ci-
          ubuntu-latest-jdk21-sbt-0.1.0-
          ubuntu-latest-jdk21-sbt-
    - name: github-packages
      run: sbt 'schema/publish; api/publish'

ZipxDocs

zipxCapabilities += ZipxDocs.pages()
zipxWorkflowDispatch := true  // Actions → Run workflow (docs without a release tag)

// Layer a fork gate; andCondition keeps the built-in tag|dispatch filter:
zipxCapabilities += ZipxDocs.pages().andCondition(JobCondition.repositoryIs("acme/libs"))

ZipxDocs.pages calls the org reusable workflow on v* tags or workflow_dispatch. Verify is skipped on dispatch so a manual run is docs-cheap; publish stays tag-only. No hand-rolled docs.yml.

DocsRender.job("docs")(ZipxDocs.pages())(using ModuleGraph(Nil))
docs:
  name: docs
  if: (startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch')
  permissions:
    contents: read
    pages: write
    id-token: write
  uses: early-effect/.github/.github/workflows/specular-docs.yml@main
  with:
    sbt-project: docs