Skip to content

Commit fb14c06

Browse files
committed
Merge branch 'deploy-in-other-org'
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
2 parents 0250bfe + adbe36e commit fb14c06

File tree

10 files changed

+86
-36
lines changed

10 files changed

+86
-36
lines changed

.funcignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
/.*
2+
/*.config.js
23
/*.md
34
/*.gif
5+
/*.svg
46
/host.json
57
/local.settings.json
8+
/package*.json
9+
/test-*.js

.github/workflows/deploy.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,24 @@ on:
88
- '.github/workflows/deploy.yml'
99
- 'GitForWindowsHelper/**'
1010

11+
permissions:
12+
contents: read
13+
id-token: write
14+
1115
jobs:
1216
deploy:
1317
if: github.event.repository.fork == false
1418
environment: deploy-to-azure
1519
runs-on: ubuntu-latest
1620
steps:
1721
- uses: actions/checkout@v6
22+
- name: 'Login via Azure CLI'
23+
uses: azure/login@v2
24+
with:
25+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
26+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
27+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
1828
- uses: Azure/functions-action@v1
1929
with:
20-
app-name: GitForWindowsHelper
21-
publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
30+
app-name: ${{ secrets.AZURE_FUNCTION_NAME || 'GitForWindowsHelper' }}
2231
respect-funcignore: true

GitForWindowsHelper/cascading-runs.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const { activeBot, activeOrg } = require('./org')
2+
13
const getToken = (() => {
24
const tokens = {}
35

@@ -12,7 +14,7 @@ const getToken = (() => {
1214
})()
1315

1416
const isAllowed = async (context, owner, repo, login) => {
15-
if (login === 'gitforwindowshelper[bot]') return true
17+
if (login === `${activeBot}[bot]`) return true
1618
const getCollaboratorPermissions = require('./get-collaborator-permissions')
1719
const token = await getToken(context, owner, repo)
1820
const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
@@ -33,7 +35,7 @@ const triggerGitArtifactsRuns = async (context, checkRunOwner, checkRunRepo, tag
3335
const owner = match[1]
3436
const repo = match[2]
3537
const workflowRunId = Number(match[3])
36-
if (owner !== 'git-for-windows' || repo !== 'git-for-windows-automation') {
38+
if (owner !== activeOrg || repo !== 'git-for-windows-automation') {
3739
throw new Error(`Unexpected repository ${owner}/${repo} for tag-git run ${tagGitCheckRun.id}: ${tagGitCheckRun.url}`)
3840
}
3941

@@ -115,12 +117,12 @@ const cascadingRuns = async (context, req) => {
115117
const checkRunRepo = req.body.repository.name
116118
const checkRun = req.body.check_run
117119
const name = checkRun.name
118-
const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === 'gitforwindowshelper'
119-
? 'gitforwindowshelper[bot]' : req.body.sender.login
120+
const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === activeBot
121+
? `${activeBot}[bot]` : req.body.sender.login
120122

121123
if (action === 'completed') {
122124
if (name === 'tag-git') {
123-
if (checkRunOwner !== 'git-for-windows' || checkRunRepo !== 'git') {
125+
if (checkRunOwner !== activeOrg || checkRunRepo !== 'git') {
124126
throw new Error(`Refusing to handle cascading run in ${checkRunOwner}/${checkRunRepo}`)
125127
}
126128

GitForWindowsHelper/component-updates.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const { activeOrg } = require('./org')
2+
13
const guessComponentUpdateDetails = (title, body) => {
24
let [ , package_name, version ] =
35
title.match(/^\[New (\S+) version\] (?:[^0-9]+\s+)?(\S+(?:\s+patch\s+\d+)?)(?! new items)/) ||
@@ -115,14 +117,14 @@ const guessReleaseNotes = async (context, issue) => {
115117
const match = issue.body.match(/See (https:\/\/\S+) for details/)
116118
if (match) return match[1]
117119

118-
const issueMatch = issue.body.match(/https:\/\/github\.com\/git-for-windows\/git\/issues\/(\d+)/)
120+
const issueMatch = issue.body.match(new RegExp(`https://github.com/${activeOrg}/git/issues/(\\d+)`))
119121
if (issueMatch) {
120122
const githubApiRequest = require('./github-api-request')
121123
const issue = await githubApiRequest(
122124
context,
123125
null,
124126
'GET',
125-
`/repos/git-for-windows/git/issues/${issueMatch[1]}`
127+
`/repos/${activeOrg}/git/issues/${issueMatch[1]}`
126128
)
127129
return matchURLInIssue(issue)
128130
}

GitForWindowsHelper/finalize-g4w-release.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
const { activeOrg, activeBot } = require('./org')
2+
13
module.exports = async (context, req) => {
24
if (req.body.action !== 'completed') return "Nothing to do here: workflow run did not complete yet"
35
if (req.body.workflow_run.conclusion !== 'success') return "Nothing to do here: workflow run did not succeed"
46

57
const releaseWorkflowRunID = req.body.workflow_run.id
6-
const owner = 'git-for-windows'
8+
const owner = activeOrg
79
const repo = 'git'
810
const sender = req.body.sender.login
911

@@ -21,7 +23,7 @@ module.exports = async (context, req) => {
2123
})()
2224

2325
const isAllowed = async (login) => {
24-
if (login === 'gitforwindowshelper[bot]') return true
26+
if (login === `${activeBot}[bot]`) return true
2527
const getCollaboratorPermissions = require('./get-collaborator-permissions')
2628
const token = await getToken()
2729
const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
@@ -31,11 +33,11 @@ module.exports = async (context, req) => {
3133
if (!await isAllowed(sender)) throw new Error(`${sender} is not allowed to do that`)
3234

3335
const { searchIssues } = require('./search')
34-
const items = await searchIssues(context, 'org:git-for-windows is:pr is:open in:comments "The release-git workflow run was started"')
36+
const items = await searchIssues(context, `org:${activeOrg} is:pr is:open in:comments "The release-git workflow run was started"`)
3537

3638
const githubApiRequest = require('./github-api-request')
3739

38-
const needle = `The \`release-git\` workflow run [was started](https://github.com/git-for-windows/git-for-windows-automation/actions/runs/${releaseWorkflowRunID})`
40+
const needle = `The \`release-git\` workflow run [was started](https://github.com/${activeOrg}/git-for-windows-automation/actions/runs/${releaseWorkflowRunID})`
3941
const candidates = []
4042
for (const item of items) {
4143
if (!['OWNER', 'MEMBER'].includes(item.author_association)) continue

GitForWindowsHelper/index.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const validateGitHubWebHook = require('./validate-github-webhook')
2+
const { activeBot, activeOrg } = require('./org')
23

34
module.exports = async function (context, req) {
45
const withStatus = (status, headers, body) => {
@@ -41,7 +42,7 @@ module.exports = async function (context, req) {
4142
if (req.headers['x-github-event'] === 'workflow_run'
4243
&& req.body.workflow_run?.event === 'workflow_dispatch'
4344
&& req.body.workflow_run?.head_branch === 'main'
44-
&& req.body.repository.full_name === 'git-for-windows/git-for-windows-automation'
45+
&& req.body.repository.full_name === `${activeOrg}/git-for-windows-automation`
4546
&& req.body.action === 'completed'
4647
&& req.body.workflow_run.path === '.github/workflows/release-git.yml'
4748
&& req.body.workflow_run.conclusion === 'success') return ok(await finalizeGitForWindowsRelease(context, req))
@@ -53,8 +54,8 @@ module.exports = async function (context, req) {
5354
try {
5455
const { cascadingRuns, handlePush } = require('./cascading-runs.js')
5556
if (req.headers['x-github-event'] === 'check_run'
56-
&& req.body.check_run?.app?.slug === 'gitforwindowshelper'
57-
&& req.body.repository.full_name === 'git-for-windows/git'
57+
&& req.body.check_run?.app?.slug === activeBot
58+
&& req.body.repository.full_name === `${activeOrg}/git`
5859
&& req.body.action === 'completed') return ok(await cascadingRuns(context, req))
5960

6061
if (req.headers['x-github-event'] === 'push'

GitForWindowsHelper/org.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
activeOrg: process.env.ACTIVE_ORG || 'git-for-windows',
3+
activeBot: process.env.ACTIVE_BOT || 'gitforwindowshelper',
4+
}

GitForWindowsHelper/slash-commands.js

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const { activeOrg } = require('./org')
2+
13
module.exports = async (context, req) => {
24
const command = req.body.comment.body
35
const owner = req.body.repository.owner.login
@@ -50,7 +52,7 @@ module.exports = async (context, req) => {
5052

5153
try {
5254
if (command === '/open pr') {
53-
if (owner !== 'git-for-windows' || !['git', 'msys2-runtime'].includes(repo)) return `Ignoring ${command} in unexpected repo: ${commentURL}`
55+
if (owner !== activeOrg || !['git', 'msys2-runtime'].includes(repo)) return `Ignoring ${command} in unexpected repo: ${commentURL}`
5456

5557
await checkPermissions()
5658

@@ -68,7 +70,7 @@ module.exports = async (context, req) => {
6870
const openPR = async (package_name, packageType) => {
6971
const { searchIssues } = require('./search')
7072
const prTitle = `${package_name}: update to ${version}`
71-
const items = await searchIssues(context, `org:git-for-windows is:pull-request "${prTitle}" in:title`)
73+
const items = await searchIssues(context, `org:${activeOrg} is:pull-request "${prTitle}" in:title`)
7274
const alreadyOpenedPR = items.filter(e => e.title === prTitle)
7375

7476
const { appendToIssueComment } = require('./issues');
@@ -91,7 +93,7 @@ module.exports = async (context, req) => {
9193
const answer = await triggerWorkflowDispatch(
9294
context,
9395
await getToken(),
94-
'git-for-windows',
96+
activeOrg,
9597
'git-for-windows-automation',
9698
'open-pr.yml',
9799
'main', {
@@ -112,7 +114,7 @@ module.exports = async (context, req) => {
112114
}
113115

114116
if (command === '/updpkgsums') {
115-
if (owner !== 'git-for-windows'
117+
if (owner !== activeOrg
116118
|| !req.body.issue.pull_request
117119
|| !['build-extra', 'MINGW-packages', 'MSYS2-packages'].includes(repo)) {
118120
return `Ignoring ${command} in unexpected repo: ${commentURL}`
@@ -125,7 +127,7 @@ module.exports = async (context, req) => {
125127
const answer = await triggerWorkflowDispatch(
126128
context,
127129
await getToken(),
128-
'git-for-windows',
130+
activeOrg,
129131
'git-for-windows-automation',
130132
'updpkgsums.yml',
131133
'main', {
@@ -142,7 +144,7 @@ module.exports = async (context, req) => {
142144

143145
const deployMatch = command.match(/^\/deploy(\s+(\S+)\s*)?$/)
144146
if (deployMatch) {
145-
if (owner !== 'git-for-windows'
147+
if (owner !== activeOrg
146148
|| !req.body.issue.pull_request
147149
|| !['build-extra', 'MINGW-packages', 'MSYS2-packages'].includes(repo)) {
148150
return `Ignoring ${command} in unexpected repo: ${commentURL}`
@@ -169,7 +171,7 @@ module.exports = async (context, req) => {
169171
await triggerWorkflowDispatch(
170172
context,
171173
await getToken(),
172-
'git-for-windows',
174+
activeOrg,
173175
'git-for-windows-automation',
174176
'build-and-deploy.yml',
175177
'main', {
@@ -223,7 +225,7 @@ module.exports = async (context, req) => {
223225
e.id = await queueCheckRun(
224226
context,
225227
await getToken(),
226-
'git-for-windows',
228+
activeOrg,
227229
repo,
228230
ref,
229231
deployLabel,
@@ -254,7 +256,7 @@ module.exports = async (context, req) => {
254256
await updateCheckRun(
255257
context,
256258
await getToken(),
257-
'git-for-windows',
259+
activeOrg,
258260
repo,
259261
e.id, {
260262
details_url: e.answer.html_url
@@ -266,7 +268,7 @@ module.exports = async (context, req) => {
266268
}
267269

268270
if (command === '/git-artifacts') {
269-
if (owner !== 'git-for-windows'
271+
if (owner !== activeOrg
270272
|| repo !== 'git'
271273
|| !req.body.issue.pull_request
272274
) {
@@ -327,7 +329,7 @@ module.exports = async (context, req) => {
327329
const answer = await triggerWorkflowDispatch(
328330
context,
329331
await getToken(),
330-
'git-for-windows',
332+
activeOrg,
331333
'git-for-windows-automation',
332334
'tag-git.yml',
333335
'main', {
@@ -389,7 +391,7 @@ module.exports = async (context, req) => {
389391
const releaseCheckRunId = await queueCheckRun(
390392
context,
391393
await getToken(),
392-
'git-for-windows',
394+
activeOrg,
393395
repo,
394396
commitSHA,
395397
'github-release',
@@ -425,7 +427,7 @@ module.exports = async (context, req) => {
425427
const owner = match[1]
426428
const repo = match[2]
427429
workFlowRunIDs[architecture] = match[3]
428-
if (owner !== 'git-for-windows' || repo !== 'git-for-windows-automation') {
430+
if (owner !== activeOrg || repo !== 'git-for-windows-automation') {
429431
throw new Error(`Unexpected repository ${owner}/${repo} for git-artifacts run ${latest.id}: ${latest.url}`)
430432
}
431433

@@ -457,7 +459,7 @@ module.exports = async (context, req) => {
457459
const answer = await triggerWorkflowDispatch(
458460
context,
459461
await getToken(),
460-
'git-for-windows',
462+
activeOrg,
461463
'git-for-windows-automation',
462464
'release-git.yml',
463465
'main', {
@@ -492,7 +494,7 @@ module.exports = async (context, req) => {
492494

493495
const relNotesMatch = command.match(/^\/add (relnote|release ?note)(\s+(blurb|feature|bug)\s+([^]*))?$/i)
494496
if (relNotesMatch) {
495-
if (owner !== 'git-for-windows'
497+
if (owner !== activeOrg
496498
|| !['git', 'build-extra', 'MINGW-packages', 'MSYS2-packages'].includes(repo)) {
497499
return `Ignoring ${command} in unexpected repo: ${commentURL}`
498500
}
@@ -519,7 +521,7 @@ module.exports = async (context, req) => {
519521
const answer = await triggerWorkflowDispatch(
520522
context,
521523
await getToken(),
522-
'git-for-windows',
524+
activeOrg,
523525
'build-extra',
524526
'add-release-note.yml',
525527
'main', {

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,27 @@ This process looks a bit complex, but the main reason for that is that three thi
111111

112112
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.
113113

114-
#### Getting the "publish profile"
114+
#### Obtaining the Azure credentials
115+
116+
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}`):
117+
118+
```shell
119+
az identity create --name <managed-identity-name> -g <resource-group>
120+
az identity federated-credential create \
121+
--identity-name <managed-identity-name> \
122+
--resource-group <resource-group> \
123+
--name github-workflow \
124+
--issuer https://token.actions.githubusercontent.com \
125+
--subject repo:<org>/gfw-helper-github-app:environment:deploy-to-azure \
126+
--audiences api://AzureADTokenExchange
127+
# The scope can be copied from the Azure Portal URL after navigating to the Azure Function
128+
az role assignment create \
129+
--assignee <client-id-of-managed-identity> \
130+
--scope '/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Web/sites/<azure-function-name>' \
131+
--role 'Contributor'
132+
```
115133

116-
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.
134+
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.
117135

118136
#### Some environment variables
119137

@@ -123,9 +141,9 @@ Concretely, the environment variables `GITHUB_WEBHOOK_SECRET`, `GITHUB_APP_PRIVA
123141

124142
### The repository
125143

126-
On https://github.com/, the `+` link on the top was pressed, and an empty, private repository was registered. Nothing was pushed to it yet.
144+
Create a fork of https://github.com/git-for-windows/gfw-helper-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).
127145

128-
After that, the contents of the publish profile that [was downloaded earlier](#getting-the-publish-profile) was registered as Actions secret, under the name `AZURE_FUNCTIONAPP_PUBLISH_PROFILE`.
146+
After that, the Azure Service Principal needs to be registered as Actions secret, under the name `AZURE_RBAC_CREDENTIALS`.
129147

130148
This repository was initialized locally only after that, actually, by starting to write this `README.md` and then developing this working toy GitHub App, and the `origin` remote was set to the newly registered repository on GitHub.
131149

__tests__/index.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,9 @@ test('a completed `tag-git` run triggers `git-artifacts` runs', async () => {
798798
slug: 'gitforwindowshelper',
799799
},
800800
},
801+
sender: {
802+
login: 'ghost'
803+
},
801804
installation: {
802805
id: 123
803806
},
@@ -999,6 +1002,9 @@ test('the third completed `git-artifacts-<arch>` check-run triggers an `upload-s
9991002
slug: 'gitforwindowshelper',
10001003
},
10011004
},
1005+
sender: {
1006+
login: 'ghost'
1007+
},
10021008
installation: {
10031009
id: 123
10041010
},

0 commit comments

Comments
 (0)