Skip to content

Commit 43da0f3

Browse files
committed
Merge branch 'vars.CONFIG'
2 parents 14d758c + a4b930c commit 43da0f3

File tree

6 files changed

+208
-34
lines changed

6 files changed

+208
-34
lines changed

.funcignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
/*.md
33
/host.json
44
/local.settings.json
5+
/package.json
6+
/package-lock.json
7+
/jest.config.js

.github/workflows/deploy.yml

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,61 @@
11
name: Deploy to Azure
22

33
on:
4+
workflow_dispatch:
45
push:
56
branches:
67
- main
78
paths:
9+
- '.funcignore'
810
- '.github/workflows/deploy.yml'
911
- 'GitGitGadget/**'
1012

13+
permissions:
14+
contents: read
15+
id-token: write
16+
1117
jobs:
1218
deploy:
13-
if: github.event.repository.fork == false
19+
if: github.event.repository.fork == false || vars.DEPLOY_WITH_WORKFLOWS != ''
1420
environment: deploy-to-azure
1521
runs-on: ubuntu-latest
1622
steps:
1723
- uses: actions/checkout@v5
24+
- name: parse DEPLOY_WITH_WORKFLOWS
25+
if: vars.DEPLOY_WITH_WORKFLOWS != '' && contains(vars.DEPLOY_WITH_WORKFLOWS, '/')
26+
id: parsed
27+
env:
28+
WORKFLOWS_REPO: '${{ vars.DEPLOY_WITH_WORKFLOWS }}'
29+
run: |
30+
echo "owner=${WORKFLOWS_REPO%%/*}" >>$GITHUB_OUTPUT &&
31+
echo "name=${WORKFLOWS_REPO#*/}" >>$GITHUB_OUTPUT
32+
- name: retrieve `vars.CONFIG` from workflows repo
33+
if: vars.DEPLOY_WITH_WORKFLOWS != ''
34+
env:
35+
WORKFLOWS_REPO: '${{ vars.DEPLOY_WITH_WORKFLOWS }}'
36+
GH_TOKEN: ${{ steps.workflows-repo-token.outputs.token || secrets.GITHUB_TOKEN }}
37+
run: |
38+
set -x &&
39+
if ! curl -fLO https://github.com/"$WORKFLOWS_REPO"/raw/config/gitgitgadget-config.json
40+
then
41+
echo "::error::Could not retrieve 'gitgitgadget-config.json' from the 'config' branch of $WORKFLOWS_REPO"
42+
exit 1
43+
fi &&
44+
jq '. + {
45+
"workflowsRepo": {
46+
"owner": "${{ steps.parsed.outputs.owner }}",
47+
"name": "${{ steps.parsed.outputs.name }}"
48+
}
49+
}' <gitgitgadget-config.json >GitGitGadget/gitgitgadget-config.json &&
50+
echo "Using the following configuration:" &&
51+
cat GitGitGadget/gitgitgadget-config.json
52+
- name: 'Login via Azure CLI'
53+
uses: azure/login@v2
54+
with:
55+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
56+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
57+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
1858
- uses: Azure/functions-action@v1
1959
with:
20-
app-name: GitGitGadget
21-
publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
60+
app-name: ${{ secrets.AZURE_FUNCTION_NAME || 'GitGitGadget' }}
2261
respect-funcignore: true
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"repo": {
3+
"name": "git",
4+
"owner": "gitgitgadget",
5+
"baseOwner": "git",
6+
"testOwner": "dscho",
7+
"owners": [
8+
"gitgitgadget",
9+
"git",
10+
"dscho"
11+
],
12+
"branches": [
13+
"maint",
14+
"seen"
15+
],
16+
"closingBranches": [
17+
"maint",
18+
"master"
19+
],
20+
"trackingBranches": [
21+
"maint",
22+
"seen",
23+
"master",
24+
"next"
25+
],
26+
"maintainerBranch": "gitster",
27+
"host": "github.com"
28+
},
29+
"mailrepo": {
30+
"name": "git",
31+
"owner": "gitgitgadget",
32+
"branch": "master",
33+
"host": "lore.kernel.org",
34+
"url": "https://lore.kernel.org/git/",
35+
"public_inbox_epoch": 1,
36+
"mirrorURL": "https://github.com/gitgitgadget/git-mailing-list-mirror",
37+
"mirrorRef": "refs/heads/lore-1",
38+
"descriptiveName": "lore.kernel/git"
39+
},
40+
"mail": {
41+
"author": "GitGitGadget",
42+
"sender": "GitGitGadget",
43+
"smtpUser": "[email protected]",
44+
"smtpHost": "smtp.gmail.com"
45+
},
46+
"app": {
47+
"appID": 12836,
48+
"installationID": 195971,
49+
"name": "gitgitgadget",
50+
"displayName": "GitGitGadget",
51+
"altname": "gitgitgadget-git"
52+
},
53+
"lint": {
54+
"maxCommitsIgnore": [
55+
"https://github.com/gitgitgadget/git/pull/923"
56+
],
57+
"maxCommits": 30
58+
},
59+
"user": {
60+
"allowUserAsLogin": false
61+
},
62+
"syncUpstreamBranches": [
63+
{
64+
"sourceRepo": "gitster/git",
65+
"targetRepo": "gitgitgadget/git",
66+
"sourceRefRegex": "^refs/heads/(maint-\\d|[a-z][a-z]/)"
67+
},
68+
{
69+
"sourceRepo": "j6t/git-gui",
70+
"targetRepo": "gitgitgadget/git",
71+
"targetRefNamespace": "git-gui/"
72+
}
73+
],
74+
"workflowsRepo": {
75+
"owner": "gitgitgadget-workflows",
76+
"name": "gitgitgadget-workflows"
77+
}
78+
}

GitGitGadget/index.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,18 @@ module.exports = async (context, req) => {
2525
}
2626

2727
try {
28-
/*
29-
* For various reasons, the GitGitGadget GitHub App can be installed
30-
* on any random repository. However, GitGitGadget only wants to support
31-
* the `gitgitgadget/git` and the `git/git` repository (with the
32-
* `dscho/git` one thrown in for debugging purposes).
33-
*/
34-
const orgs = ['gitgitgadget', 'git', 'dscho']
35-
const a = [context, undefined, 'gitgitgadget-workflows', 'gitgitgadget-workflows']
28+
const { readFileSync } = require('fs')
29+
const config = JSON.parse(readFileSync(`${__dirname}/gitgitgadget-config.json`))
30+
const orgs = config.repo.owners
31+
const a = [context, undefined, config.workflowsRepo.owner, config.workflowsRepo.name]
3632

3733
const eventType = context.req.headers['x-github-event'];
3834
context.log(`Got eventType: ${eventType}`);
3935
const repositoryOwner = req.body.repository.owner.login;
4036
if (!orgs.includes(repositoryOwner)) {
4137
context.res = {
4238
status: 403,
43-
body: 'Refusing to work on a repository other than gitgitgadget/git or git/git'
39+
body: `Refusing to work on any repository outside of ${orgs.join(', ')}`,
4440
};
4541
} else if (eventType === 'pull_request') {
4642
if (req.body.action !== 'opened' && req.body.action !== 'synchronize') {
@@ -58,9 +54,9 @@ module.exports = async (context, req) => {
5854
body: `Ignored event type: ${eventType}`,
5955
};
6056
} else if (eventType === 'push') {
61-
if (req.body.repository.full_name ==='gitgitgadget/git-mailing-list-mirror') {
57+
if (config.mailrepo.mirrorURL === `https://github.com/${req.body.repository.full_name}`) {
6258
context.res = { body: `push(${req.body.ref} in ${req.body.repository.full_name}): ` }
63-
if (req.body.ref === 'refs/heads/lore-1') {
59+
if (req.body.ref === config.mailrepo.mirrorRef) {
6460
const queued = await listWorkflowRuns(...a, 'handle-new-mails.yml', 'queued')
6561
if (queued.length) {
6662
context.res.body += [
@@ -72,7 +68,7 @@ module.exports = async (context, req) => {
7268
context.res.body += `triggered ${run.html_url}`
7369
}
7470
} else context.res.body += `Ignoring non-default branches`
75-
} else if (req.body.repository.full_name !== 'git/git') {
71+
} else if (req.body.repository.full_name !== `${config.repo.baseOwner}/${config.repo.name}`) {
7672
context.res = { body: `Ignoring pushes to ${req.body.repository.full_name}` }
7773
} else {
7874
const run = await triggerWorkflowDispatch(
@@ -83,7 +79,7 @@ module.exports = async (context, req) => {
8379
}
8480
)
8581
const extra = []
86-
if (req.body.ref === 'refs/heads/seen') {
82+
if (config.repo.branches.map((name) => `refs/heads/${name}`).includes(req.body.ref)) {
8783
for (const workflow of ['update-prs.yml', 'update-mail-to-commit-notes.yml']) {
8884
if ((await listWorkflowRuns(...a, workflow, 'main', 'queued')).length === 0) {
8985
const run = await triggerWorkflowDispatch(...a, workflow, 'main')
@@ -102,7 +98,7 @@ module.exports = async (context, req) => {
10298
}
10399

104100
/* GitGitGadget works on dscho/git only for testing */
105-
if (repositoryOwner === 'dscho' && comment.user.login !== 'dscho') {
101+
if (repositoryOwner === config.repo.testOwner && comment.user.login !== config.repo.testOwner) {
106102
throw new Error(`Ignoring comment from ${comment.user.login}`);
107103
}
108104

README.md

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,42 +45,100 @@ Finally, [run the Function locally](https://learn.microsoft.com/en-us/azure/azur
4545

4646
You can also run/debug it via VS Code, there is a default configuration called "Attach to Node Functions".
4747

48-
## How this GitHub App was set up
48+
## How to set up this GitHub App
4949

5050
This process looks a bit complex, the main reason for that being that three things have to be set up essentially simultaneously: an Azure Function, a GitHub repository and a GitHub App.
5151

5252
### The Azure Function
5353

54-
First of all, a new [Azure Function](https://portal.azure.com/#blade/HubsExtension/BrowseResourceBlade/resourceType/Microsoft.Web%2Fsites/kind/functionapp) was created. A Linux one was preferred, for cost and performance reasons. Deployment with GitHub was _not_ yet configured.
55-
56-
#### Getting the "publish profile"
54+
First of all, a new [Azure Function](https://portal.azure.com/#blade/HubsExtension/BrowseResourceBlade/resourceType/Microsoft.Web%2Fsites/kind/functionapp) needs to be created. A Linux one is preferred, with a regular Consumption plan, for [cost](https://azure.microsoft.com/en-us/pricing/details/functions/) and performance reasons. Deployment with GitHub should _not_ yet be configured.
55+
56+
#### Obtaining the Azure credentials
57+
58+
The idea is to use [OpenID Connect](https://docs.github.com/en/actions/concepts/security/openid-connect) to log into Azure in the deploy workflow, _identifying_ as said workflow, via a "Managed Identity". This can be registered after the Azure Function has been successfully created: In an Azure CLI (for example [the one that is very neatly embedded in the Azure Portal](https://learn.microsoft.com/en-us/azure/cloud-shell/get-started/classic)), run this (after replacing the placeholders `{subscription-id}`, `{resource-group}` and `{app-name}`):
59+
60+
```shell
61+
az identity create --name <managed-identity-name> -g <resource-group>
62+
az identity federated-credential create \
63+
--identity-name <managed-identity-name> \
64+
--resource-group <resource-group> \
65+
--name github-workflow \
66+
--issuer https://token.actions.githubusercontent.com \
67+
--subject repo:<org>/gitgitgadget-github-app:environment:deploy-to-azure \
68+
--audiences api://AzureADTokenExchange
69+
# The scope can be copied from the Azure Portal URL after navigating to the Azure Function
70+
az role assignment create \
71+
--assignee <client-id-of-managed-identity> \
72+
--scope '/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Web/sites/<azure-function-name>' \
73+
--role 'Contributor'
74+
```
5775

58-
After the deployment succeeded, in the "Overview" tab, there is a "Get publish profile" link on the right panel at the center top. Clicking it will automatically download a `.json` file whose contents will be needed later.
76+
The result is a "managed identity", essentially a tightly-scoped credential that allows deploying this particular Azure Function from that particular repository in a GitHub workflow run and that's it. This managed identity is identified via the `AZURE_CLIENT_ID`, `AZURE_TENANT_ID` and `AZURE_SUBSCRIPTION_ID` Actions secrets, more on that below.
5977

6078
#### Some environment variables
6179

62-
A few environment variables will have to be configured for use with the Azure Function. This can be done on the "Configuration" tab, which is in the "Settings" group.
80+
A few environment variables need to be configured for use with the Azure Function. This can be done on the "Configuration" tab, which is in the "Settings" group.
6381

64-
Concretely, the environment variables `GITHUB_WEBHOOK_SECRET` and `GITGITGADGET_TRIGGER_TOKEN` (a Personal Access Token to trigger the Azure Pipelines) need to be set. For the first, a generated random string was used. The second one was [created](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#create-a-pat) scoped to the Azure DevOps project `gitgitgadget` with the Build (read & execute) permissions.
82+
Concretely, the environment variables `GITHUB_WEBHOOK_SECRET` needs to be set, any generated random string can be used as its value (and the same value needs to be used when eventually registering the actual GitHub App).
6583

66-
Also, the `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` variables are needed in order to trigger GitHub workflow runs. These were obtained as part of registering the GitHub App.
84+
Also, the `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` variables are needed in order to trigger GitHub workflow runs. These are obtained as part of registering the GitHub App (see below).
6785

6886
### The repository
6987

70-
On https://github.com/, the `+` link on the top was pressed, and an empty, private repository was registered. Nothing was pushed to it yet.
88+
Create a fork of https://github.com/gitgitgadget/gitgitgadget-github-app. Configure the Azure Managed Identity via Actions secrets, under the keys `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_SUBSCRIPTION_ID`. Also, the `AZURE_FUNCTION_NAME` secret needs to be defined (its value is the name of the Azure Function).
7189

72-
After that, the contents of the publish profile that [was downloaded earlier](#getting-the-publish-profile) were registered as Actions secret, under the name `AZURE_FUNCTIONAPP_PUBLISH_PROFILE`.
90+
Also configure the repository _variable_ `DEPLOY_WITH_WORKFLOWS`; Its value must correspond to the fork of https://github.com/gitgitgadget/gitgitgadget-workflows, in the form `<org>/gitgitgadget-workflows`. Note that that fork _must_ have a `config` branch that contains a valid project configuration in its `gitgitgadget-config.json` file.
7391

74-
This repository was initialized locally by forking https://github.com/gitgitgadget/gitgitgadget and separating out the Azure Functions part of it. Then, the test suite was developed and the GitHub workflows were adapted from https://github.com/git-for-windows/gfw-helper-github-app. After that, the `origin` remote was set to the newly registered repository on GitHub.
75-
76-
As a last step, the repository was pushed, triggering the deployment to the Azure Function.
92+
As a last step, on the Actions tab, the `Deploy to Azure` workflow needs to be triggered manually, which deploys the Azure Function.
7793

7894
### The GitHub App
7995

80-
Finally, the existing GitHub App's webhook URL was redirected to the new one. If there had not been an existing GitHub App, [a new GitHub App would have been registered](https://github.com/settings/apps/new) with https://github.com/gitgitgadget as homepage URL.
96+
Now it is finally time to [register a new GitHub App](https://github.com/settings/apps/new) with https://github.com/<org>> as homepage URL.
8197

82-
As Webhook URL, the URL of the Azure Function was used, which can be copied in the "Functions" tab of the Azure Function. It looks similar to this: https://my-github-app.azurewebsites.net/api/MyGitHubApp
98+
As Webhook URL, use the URL of the Azure Function, which should look like this: https://<azure-function-name>.azurewebsites.net/api/GitGitGadget
8399

84100
The value stored in the Azure Function as `GITHUB_WEBHOOK_SECRET` was used as Webhook secret.
85101

86-
The GitGitGadget GitHub app requires the following permissions: Read access to metadata, and Read and write access to checks, code, commit statuses, issues, pull requests, and workflows.
102+
The GitGitGadget GitHub app requires the following permissions: Read access to metadata, Read and write access to Variables, Actions, Checks, Commit statuses, Contents, Issues, Pull requests, and Workflows. It needs the following webhook events to be enabled: Check run, Commit comment, Issue comment, Pull request, Pull request review, Pull request review comment, Push, Repository, and Status.
103+
104+
Once the GitHub App is successfully registered (and unfortunately only then), the private key can be generated via clicking the `Generate a private key` button in the "Private keys" section toward the bottom. This will automatically download a file; The contents of that file, with newlines replaced by `\n`, need to be configured as `GITHUB_APP_PRIVATE_KEY` environment variable in your Azure Function's `Settings>Environment variables` tab, and `GITHUB_APP_ID` needs to be set, too (it can be seen on the GitHub App's page at the top, labeled as "App ID").
105+
106+
The app needs to be installed on the fork of the `gitgitgadget-workflows` repository, and the app ID and private key should also be stored as Actions secrets in the fork of the `gitgitgadget-github-app` repository and it should be re-deployed so that it can pick up those new bits and pieces.
107+
108+
#### Using `register-github-app-cli`
109+
110+
A convenient alternative to clicky-clicky in the GitHub UI to register the GitHub is the convenient [`npx register-github-app-cli` command](https://github.com/gr2m/register-github-app-cli): Use it with `--org <owning-organization>` and a variation of this manifest:
111+
112+
```yml
113+
name: <name>
114+
url: https://github.com/apps/<name>
115+
hook_attributes:
116+
url: https://<function-app-name>.azurewebsites.net/api/GitGitGadget
117+
public: false
118+
default_permissions:
119+
actions: write
120+
checks: write
121+
commit_statuses: write
122+
contents: write
123+
issues: write
124+
metadata: read
125+
pull_requests: write
126+
variables: read
127+
workflows: write
128+
default_events:
129+
- check_run
130+
- commit_comment
131+
- issue_comment
132+
- pull_request
133+
- pull_request_review
134+
- pull_request_review_comment
135+
- push
136+
- repository
137+
- status
138+
```
139+
140+
### A read-only GitHub App
141+
142+
In complex setups, like the one for the Git project, there is more than one repository in which users open Pull Requests that then get forwarded by GitGitGadget: In addition to the "pr-repo", there can be an "upstream-repo" that is owned by the upstream project and should not allow GitGitGadget to write to it.
143+
144+
To this end, a second GitHub App can be registered, one that lacks all permissions except Read/write on `issues` & `pull_requests` (to write PR comments) and `checks` (to mirror the workflow runs to the PRs). This will need to be configured on the `gitgitgadget-workflows` fork as `GITGITGADGET_READONLY_GITHUB_APP_ID` and `GITGITGADGET_READONLY_GITHUB_APP_PRIVATE_KEY` secrets.

__tests__/index.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ testIssueComment('/test', async (context) => {
162162
testIssueComment('/verify-repository', 'nope', (context) => {
163163
expect(context.done).not.toHaveBeenCalled()
164164
expect(context.res).toEqual({
165-
body: 'Refusing to work on a repository other than gitgitgadget/git or git/git',
165+
body: 'Refusing to work on any repository outside of gitgitgadget, git, dscho',
166166
'status': 403,
167167
})
168168
expect(mockRequest.write).not.toHaveBeenCalled()

0 commit comments

Comments
 (0)