Support GitHub Apps for Authentication in Crowdin GitHub Action
Feature Request: Support GitHub Apps for Authentication in Crowdin GitHub Action
Is your feature request related to a problem? Please describe.
Currently, the Crowdin GitHub Action requires a Personal Access Token (PAT) for authentication, which introduces security risks and compliance challenges. Many organizations, including those using GitHub Enterprise Managed Users (EMU), are transitioning away from PATs in favor of GitHub Apps, which provide:
- Scoped permissions to minimize security risks.
- Automatic token rotation to reduce manual management.
- Better compliance with enterprise security policies.
Without GitHub Apps support, teams are forced to manage long-lived PATs, increasing the risk of token leaks, adding operational overhead, and potentially preventing adoption in environments with strict security policies.
This is critical for us due to security concerns, and we may be unable to continue using the Crowdin GitHub Action if GitHub Apps authentication is not supported.
Describe the solution you’d like
We request official support for GitHub Apps authentication in Crowdin GitHub Action, allowing:
- Authentication via GitHub App tokens instead of manually managed PATs.
-
Support for
GITHUB_TOKENin workflows, leveraging GitHub’s built-in authentication with appropriate permissions. - A migration path from PAT-based authentication to GitHub Apps for existing users.
Describe alternatives you’ve considered
- Continuing with PATs: Not ideal due to security risks, manual token rotation, and GitHub's push towards deprecating PAT usage in automation.
Additional context
Crowdin’s official GitHub Action (crowdin/github-action) currently requires PATs for pushing translations and creating pull requests.
-
GitHub officially recommends GitHub Apps for automation and has enabled
GITHUB_TOKENauthentication for repository access. - Many organizations, including ours, will be unable to use Crowdin GitHub Action if GitHub Apps authentication is not supported.
This feature is critical for us, and we’d love to hear if it's on the roadmap. We're happy to assist with testing to ensure a smooth transition.
Looking forward to your response. Thanks!
Hi @Paul-Weaver, thanks for the request!
There was a similar request in issue #264. Could you please check if the solution suggested in that issue works for you?
Hi @Paul-Weaver, thanks for the request!
There was a similar request in issue #264. Could you please check if the solution suggested in that issue works for you?
Thanks for the swift response, I will create GitHub App then test it
@andrii-bodnar it actually works on latest crowdin using GITHUB_TOKEN and having the app token there. Or am I misunderstanding something?
name: crowdin-test-run
on:
workflow_dispatch:
jobs:
crowdin-test:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
pull_request_number: ${{ steps.download_translations.outputs.pull_request_number }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Generate GitHub App Token (Crowdin Test)
id: generate-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.CROWDIN_APP_ID }}
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
- id: download_translations
name: Download Translations & Create PR (Crowdin Test)
uses: crowdin/github-action@v2
with:
upload_sources: true
download_translations: true
upload_translations: false
download_translations_args: >
--language=de
create_pull_request: true
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
@Paul-Weaver it's interesting 🤔 Based on the mentioned issue, I thought it was not working this way
@Paul-Weaver it's interesting 🤔 Based on the mentioned issue, I thought it was not working this way
I'm not sure, but you can see they don't have the correct permissions anyhow it's missing pull-requests: write
@Paul-Weaver indeed, that looks like a reason. So it seems to work without any changes to the action code.
By the way, would you like to contribute a readme update describing how to authenticate via the GitHub app? It would be very helpful for other developers.
@Paul-Weaver indeed, that looks like a reason. So it seems to work without any changes to the action code.
By the way, would you like to contribute a readme update describing how to authenticate via the GitHub app? It would be very helpful for other developers.
Sure I can help with that!
@Paul-Weaver Did you happen to have a chance to write such docs? I'm struggling to understand why this doesn't work:
jobs:
crowdin-test:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
Here we see the job has readonly access.
But suppose I supply a GitHub App access token that I am absolutely certain has contents: write and pull-requests: write on the relevant repo (assume I obtain one and store it in steps.generate-token.outputs.token):
- name: Crowdin
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2
with: [snip]
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
I get a 403 on trying to write to the repo.
My suspicion is that one cannot overwrite GITHUB_TOKEN like that, which @lol3909 noted here. I note that in your (Paul's) comment you have write access in the permissions block and I wonder if that's why it worked for you there?
@andrii-bodnar Is there any possibility of being able to supply GITHUB_TOKEN as GH_TOKEN in the case where we want to override it in our jobs? That might achieve my desired behaviour, which is:
- Start a job with very low permissions (readonly)
- Make a call to obtain a write-access token
- Pass that token to Crowdin (i.e. a "stepped-up"
GITHUB_TOKEN)
Hi @3dbrows, In the related issue, there was a proof of concept for this. Could you please check to see if that solution works for you?
Thanks very much @andrii-bodnar , I will try that and report back; I am wondering if this will work in light of what GitHub say here about restrictions on variable names:
You can't overwrite the value of the default environment variables named GITHUB_* and RUNNER_*.
It's not super clear to me from that whether overwrite is used in a strict sense, or it means you also can't write them with original values.
So, it might object to GITHUB_APP_TOKEN but not to GH_TOKEN or similar, because the latter doesn't match GITHUB_*. We'll see!
@3dbrows I just merged your PR into the app_token branch. Could you please check it out?
I've been struggling with this but I don't think it's any problem with the action; more what limitations GitHub Actions puts in place.
I tried with the following, but it 403's every time. Interestingly, if I get a token locally and try pushing to a REPO_URL containing github.com rather than api.github.com, it works. I also tried using different GITHUB_ACTOR values in the REPO_URL, but it made no difference.
I'm left with the suspicion that GitHub Actions does not let you escalate privileges by generating a token with broader permissions than what is given in the job's permissions stanza... but I also have big doubts about that, because how would the remote Git origin know?
And yet, locally, using git push https://any-username:[email protected]/foo/bar.git does work. Just not in an Action. (Using api.github.com works in neither place.) For what it's worth, my token starts with ghs_ and I'm on GitHub Enterprise SaaS.
At this point, I'm sorry to say, I'm out of ideas... so we might be forced to just use contents: write in the job permissions.
- uses: actions/[email protected]
id: generate-token
with:
app-id: "<redacted>"
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write
- name: crowdin action
uses: crowdin/github-action@app_token
with:
config: crowdin.yml
github_api_base_url: github.com # I also tried omitting this line
upload_sources: false
download_translations: true
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
Thanks for looking at this with me!
@3dbrows here’s what worked for me:
- Two tokens, two roles
-
GITHUB_TOKEN(default) handles the REST / GraphQL API calls (open/update PR, set commit status). -
GH_TOKEN(GitHub App token) handles the raw git push. IfGITHUB_TOKEN(default token is read-only), the API call that opens the PR will 403—even though the push itself succeeded.
-
Give the job write permissions
Add “contents: write” and “pull-requests: write” to the job.
This upgrades the default token just long enough for the API work whileGH_TOKENstays scoped for the push. -
Drop
github_api_base_url: Let the action use its default endpoint (api.github.com) unless you’re on a self-hosted GHES instance.
Fix
Give the job write perms so the default token can open/update the PR:
name: crowdin-sync
on:
workflow_dispatch:
jobs:
crowdin:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.CROWDIN_APP_ID }}
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write # keeps token scoped
- name: Crowdin sync
uses: crowdin/github-action@app_token
with:
config: crowdin.yml
upload_sources: false
download_translations: true
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
Give that a spin and shout if anything’s still off.
TL;DR — The Crowdin action needs two tokens: the default GITHUB_TOKEN for API calls (opening/updating the PR) and a scoped GH_TOKEN from your GitHub App for the actual git push. If the job’s permissions are read-only, those API calls 403 even though the push works. Give the job temporary contents: write and pull-requests: write perms so GITHUB_TOKEN can finish its work, keep GH_TOKEN for the push, and drop github_api_base_url unless you’re on GHES. That combo removes the 403.
Thanks Paul. That does, indeed, work. I'd conclude from this that if the action were to use GH_TOKEN for everything, (though I thought it did) obtained from a correctly configured app (with permissions to write PRs and pushes), then the following would be possible.
The main (only?) advantage being that it scores maximum points in terms of token scope reduction.
name: crowdin-sync
on:
workflow_dispatch:
jobs:
crowdin:
runs-on: ubuntu-latest
permissions:
contents: read # to allow the checkout to work
steps:
- uses: actions/checkout@v4
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.CROWDIN_APP_ID }}
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Crowdin sync
uses: crowdin/github-action@app_token
with:
config: crowdin.yml
upload_sources: false
download_translations: true
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
Great work on these findings, @Paul-Weaver and @3dbrows. Thank you! 🚀