Skip to content

Working with Forks

When contributing to an API repository you don't own, you'll typically work on a fork — a personal copy of the canonical repo under your own GitHub organization or user account. APX is fork-aware and handles most of this automatically, but there are important limitations around releasing.

How Fork Detection Works

When you run apx init (or any command that needs org/repo defaults), APX inspects your git remotes:

  1. origin — your fork (e.g. git@github.com:your-user/apis.git)
  2. upstream — the canonical repo (e.g. git@github.com:acme-corp/apis.git)

If both remotes exist and point to different organizations, APX assumes you're working on a fork and automatically uses the upstream org for all consumption paths. This ensures generated import paths, go_package options, and dependency references all point to the canonical repository — not your fork.

# Your remotes
origin    → git@github.com:your-user/apis.git      (your fork)
upstream  → git@github.com:acme-corp/apis.git       (canonical)

# APX detects:
#   Org = acme-corp  (from upstream, not your-user)
#   Repo = apis
#   UpstreamOrg = acme-corp

Setting Up Your Fork

# Fork the canonical repo on GitHub, then:
git clone git@github.com:<your-user>/apis.git
cd apis
git remote add upstream git@github.com:<canonical-org>/apis.git

# APX will now auto-detect the canonical org
apx init canonical --non-interactive
# → org: <canonical-org>  (detected from upstream remote)

What Works on a Fork

All consumption and authoring workflows work correctly on forks:

Workflow Status Details
apx init Works Auto-detects canonical org from upstream remote
apx lint Works Schema validation is local
apx breaking Works Compatibility checks against local refs
apx gen Works Code generation uses canonical import paths
apx show Works Displays canonical identity correctly
apx inspect identity Works Shows canonical go_package and module paths
apx explain go-path Works Derives paths from canonical org/repo
apx search Works Queries the catalog
Local development Works go.work overlays resolve to local code

What Does NOT Work on a Fork

Releasing from a fork is not supported. Several operations require write access to the canonical repository, which fork contributors typically don't have:

1. apx release submit — Release Submission

apx release submit pushes a release branch (apx/release/<api-id>/<version>) to the canonical repo and opens a PR. Fork contributors cannot push branches to a repo they don't own.

2. apx release tag — Tag Creation

Release tags (e.g. proto/payments/ledger/v1/v1.0.0) are created on the canonical repo by post-merge CI. Fork contributors cannot create tags on a repo they don't own.

3. CI-Triggered Releasing

CI workflows on your fork run with your fork's credentials, not the canonical repo's. Any CI step that calls apx release submit will fail because the fork's GITHUB_TOKEN doesn't have write access to the upstream repo.

The correct pattern is to author on the fork, release from the canonical repo's CI:

┌─────────────────────────┐     ┌──────────────────────────┐
│    Your Fork              │     │  Canonical Repo           │
│                           │     │                            │
│  1. Author schemas        │     │                            │
│  2. apx lint              │     │                            │
│  3. apx breaking          │     │                            │
│  4. apx gen go            │     │                            │
│  5. git push origin       │     │                            │
│                           │     │                            │
│  6. Open PR ─────────────────── │  7. CI validates PR        │
│                           │     │  8. Reviewers approve      │
│                           │     │  9. Merge to main          │
│                           │     │ 10. Post-merge CI:         │
│                           │     │     - apx release finalize │
│                           │     │     - apx catalog generate │
│                           │     │     - tag creation          │
└─────────────────────────┘     └──────────────────────────┘

Steps 1–6 happen on your fork with your credentials. All consumption-side commands work because APX resolves the canonical org from your upstream remote.

Steps 7–10 happen on the canonical repo's CI, which has the GitHub App credentials to create tags, update the catalog, and finalize releases.

Example CI Configuration (Canonical Repo)

The canonical repo uses two workflows generated by apx init canonical:

PR validation (.github/workflows/ci.yml):

name: APX Schema CI
on:
  pull_request:
    branches: [main]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: infobloxopen/apx@main
      - run: apx lint
      - run: apx breaking --against origin/main

Post-merge catalog build & publish (.github/workflows/on-merge.yml):

name: APX On Merge
on:
  push:
    branches: [main]
permissions:
  contents: read
  packages: write
  id-token: write
  attestations: write
jobs:
  catalog:
    runs-on: ubuntu-latest
    env:
      IMAGE: ghcr.io/<org>/${{ github.event.repository.name }}-catalog
    steps:
      - uses: actions/create-github-app-token@v1
        id: app-token
        with:
          app-id: ${{ secrets.APX_APP_ID }}
          private-key: ${{ secrets.APX_APP_PRIVATE_KEY }}
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ steps.app-token.outputs.token }}
      - uses: infobloxopen/apx@main
      - run: apx lint
      - run: apx catalog generate --output catalog/catalog.yaml
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - run: |
          docker build \
            --build-arg CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
            --build-arg REVISION="${{ github.sha }}" \
            --build-arg SOURCE="https://github.com/${{ github.repository }}" \
            --build-arg VERSION="${{ github.sha }}" \
            -t "$IMAGE:latest" \
            -t "$IMAGE:sha-${GITHUB_SHA::7}" \
            catalog/
      - id: push
        run: |
          docker push "$IMAGE:latest"
          docker push "$IMAGE:sha-${GITHUB_SHA::7}"
          DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE:latest" | cut -d@ -f2)
          echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
      - uses: actions/attest-build-provenance@v2
        with:
          subject-name: ${{ env.IMAGE }}
          subject-digest: ${{ steps.push.outputs.digest }}
          push-to-registry: true
      - uses: anchore/sbom-action@v0
        with:
          image: ${{ env.IMAGE }}:latest
          output-file: sbom.spdx.json
      - uses: actions/attest-sbom@v2
        with:
          subject-name: ${{ env.IMAGE }}
          subject-digest: ${{ steps.push.outputs.digest }}
          sbom-path: sbom.spdx.json
          push-to-registry: true

See CI Templates for details on these workflows.

Troubleshooting

APX uses my fork org instead of the canonical org

Ensure you have an upstream remote pointing to the canonical repo:

git remote -v
# Should show:
# origin    git@github.com:<your-user>/apis.git (fetch)
# upstream  git@github.com:<canonical-org>/apis.git (fetch)

# If upstream is missing:
git remote add upstream git@github.com:<canonical-org>/apis.git

apx release submit fails with "permission denied"

You're likely running the release from a fork. Releasing must happen from the canonical repo's CI after your PR is merged. See Recommended Fork Workflow above.

Import paths show my fork org in generated code

This means APX didn't detect the fork. Check that: 1. The upstream remote exists and points to the canonical repo 2. The upstream org is different from origin — APX only overrides when orgs differ 3. Run apx inspect identity to verify the resolved org