# GitHub Actions and Private Submodules: Fine-Grained Token Setup Or, "why the heck are simple things hard"? My organisation has a private repo, let's call it "shared". This contains shared code like protobuf definitions, API clients, common types, and similar things. It doesn't make sense for them to sit in a singular repo so it has been broken out into its own. Deploying the "main" repos requires cloning this shared repo as a submodule. This was much more difficult than it should've been, so I wrote up this blog post so future me won't be so frustrated trying to figure it out. ## The Old Way: Personal Access Tokens Nice and simple, but they expire quite quickly, are tied to the user and usually have too much permissions. ```yaml jobs: deploy: # needs: wait-on-checks runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v3 with: fetch-depth: 0 submodules: recursive token: ${{ secrets.GH_PAT }} ``` ## The New Way: Fine-Grained Tokens It's much more difficult than you think! The permissions are a lot more precise (obviously). The token also lives on the organisation, so you don't have to tie company processes to your GitHub account. ### Token Setup Grant the token access on the specific submodule you want, and permit it "content read" and "metadata" scopes. ### Workflow Changes Change your CI workflow: - First, clone the repo without submodules - Then, replace your SHARED_TOKEN as the global git user - Next, replace SSH git module URL schemes with HTTPS ```yaml jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: persist-credentials: true submodules: false - name: Configure Git for submodule access run: | # Configure Git to use token for GitHub URLs - do this FIRST git config --global url."https://${{ secrets.SHARED_TOKEN }}@github.com/".insteadOf "https://github.com/" # Update submodule URL from SSH to HTTPS git config --file=.gitmodules submodule.v3-shared.url https://github.com/nii236/shared.git # Sync the changes git submodule sync --recursive - name: Checkout submodules run: | # Now fetch the submodules git -c protocol.version=2 submodule update --init --force --depth=1 --recursive - name: Remove Git token configuration if: always() run: | git config --global --unset url."https://${{ secrets.SHARED_TOKEN }}@github.com/".insteadOf ``` The key thing is doing the URL rewriting before trying to update the submodules. The cleanup step at the end removes the global git config so you don't leak tokens. ## Conclusion Fine-grained tokens are more secure and better for organizations, but the setup is way more involved than it should be. Once you get this pattern working though, you can copy it to any repo that needs private submodules.