Skip to content

Commit

Permalink
Add bootstrap-pull-request action (#1167)
Browse files Browse the repository at this point in the history
  • Loading branch information
int128 authored Oct 24, 2023
1 parent 62d0be7 commit f6b63df
Show file tree
Hide file tree
Showing 18 changed files with 876 additions and 0 deletions.
92 changes: 92 additions & 0 deletions .github/workflows/bootstrap-pull-request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: bootstrap-pull-request

on:
pull_request:
paths:
- bootstrap-pull-request/**
- '*.json'
- .github/workflows/bootstrap-pull-request.yaml
push:
branches:
- main
paths:
- bootstrap-pull-request/**
- '*.json'
- .github/workflows/bootstrap-pull-request.yaml

defaults:
run:
working-directory: bootstrap-pull-request

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version: 20
cache: yarn
- run: yarn
- run: yarn test

e2e-test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version: 20
cache: yarn
- run: yarn
- run: yarn build
- run: yarn package

- run: |
git config --global user.email '[email protected]'
git config --global user.name 'github-actions'
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
ref: ${{ github.head_ref }} # avoid "shallow update not allowed" error
path: prebuilt-branch
- name: Set up an prebuilt branch
working-directory: prebuilt-branch
run: |
mkdir -vp services/a
mkdir -vp services/b
touch services/a/generated.yaml
touch services/b/generated.yaml
git add .
git commit -m "Add prebuilt branch for e2e-test of ${GITHUB_REF}"
git push origin "HEAD:refs/heads/prebuilt/monorepo-deploy-actions/overlay-${{ github.run_id }}"
- uses: ./bootstrap-pull-request
with:
overlay: overlay-${{ github.run_id }}
namespace: pr-${{ github.event.number }}
destination-repository: ${{ github.repository }}
namespace-manifest: bootstrap-pull-request/tests/fixtures/namespace.yaml
substitute-variables: NAMESPACE=pr-${{ github.event.number }}

# the action should be idempotent
- uses: ./bootstrap-pull-request
with:
overlay: overlay-${{ github.run_id }}
namespace: pr-${{ github.event.number }}
destination-repository: ${{ github.repository }}
namespace-manifest: bootstrap-pull-request/tests/fixtures/namespace.yaml
substitute-variables: NAMESPACE=pr-${{ github.event.number }}

- name: Clean up the namespace branch
continue-on-error: true
if: always()
run: |
git push origin --delete "refs/heads/ns/monorepo-deploy-actions/overlay-${{ github.run_id }}/pr-${{ github.event.number }}"
- name: Clean up the prebuilt branch
continue-on-error: true
if: always()
run: |
git push origin --delete "refs/heads/prebuilt/monorepo-deploy-actions/overlay-${{ github.run_id }}"
137 changes: 137 additions & 0 deletions bootstrap-pull-request/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# bootstrap-pull-request [![bootstrap-pull-request](https://github.com/quipper/monorepo-deploy-actions/actions/workflows/bootstrap-pull-request.yaml/badge.svg)](https://github.com/quipper/monorepo-deploy-actions/actions/workflows/bootstrap-pull-request.yaml)

This is an action to bootstrap the pull request namespace.
When a pull request is created or updated, this action copies the service manifests from the prebuilt branch.

```mermaid
graph LR
subgraph Source repository
SourceNamespaceManifest[Namespace manifest]
end
subgraph Destination repository
subgraph Prebuilt branch
PrebuiltServiceManifest[Service manifest]
end
subgraph Namespace branch
ApplicationManifest[Application manifest]
ServiceManifest[Service manifest]
NamespaceManifest[Namespace manifest]
end
end
PrebuiltServiceManifest --Copy--> ServiceManifest
SourceNamespaceManifest --Copy--> NamespaceManifest
```

## Getting Started

To bootstrap the pull request namespace,

```yaml
name: pr-namespace / bootstrap

on:
pull_request:

jobs:
bootstrap-pull-request:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: quipper/monorepo-deploy-actions/bootstrap-pull-request@v1
with:
overlay: pr
namespace: pr-${{ github.event.number }}
destination-repository: octocat/generated-manifests
destination-repository-token: ${{ steps.destination-repository-github-app.outputs.token }}
namespace-manifest: deploy-config/overlays/pr/namespace.yaml
substitute-variables: |
NAMESPACE=pr-${{ github.event.number }}
```
This action creates a namespace branch into the destination repository.
```
ns/${source-repository}/${overlay}/${namespace-prefix}${pull-request-number}
```

It creates the following directory structure.

```
.
├── applications
| ├── namespace.yaml
| └── ${namespace}--${service}.yaml
└── services
└── ${service}
└── generated.yaml
```

It assumes that the below name of prebuilt branch exists in the destination repository.

```
prebuilt/${source-repository}/${overlay}
```

It bootstraps the namespace branch by the following steps:

- Copy the services from prebuilt branch.
- Write the namespace manifest

### Copy the services from prebuilt branch

This action copies the services from prebuilt branch to the namespace branch.

For example, if the prebuilt branch has 2 services `backend` and `frontend`,
the namespace branch will be the below structure.

```
.
├── applications
| ├── pr-123--backend.yaml
| └── pr-123--frontend.yaml
└── services
├── backend
| └── generated.yaml
└── frontend
└── generated.yaml
```

All placeholders will be replaced during copying the service manifests.
For example, if `NAMESPACE=pr-123` is given by `substitute-variables` input,
this action will replace `${NAMESPACE}` with `pr-123`.

If a service was pushed by `git-push-service` action,
this action does not overwrite it.

**Case 1**: If a service is not changed in a pull request,

1. When a pull request is created, this action copies the service from prebuilt branch.
1. When the pull request is updated, this action copies the service from prebuilt branch.
This is needed to follow the latest change of prebuilt branch.

**Case 2**: If a service is changed in a pull request,

1. When a pull request is created,
- This action copies the service from prebuilt branch.
- `git-push-service` action overwrites the service.
1. When the pull request is synchronized,
- This action does not overwrite the service.

### Write the namespace manifest

This action copies the namespace manifest to path `/applications/namespace.yaml` in the namespace branch.

```
.
└── applications
└── namespace.yaml
```

All placeholders will be replaced during copying the namespace manifest.
For example, if `NAMESPACE=pr-123` is given by `substitute-variables` input,
this action will replace `${NAMESPACE}` with `pr-123`.

## Specification

See [action.yaml](action.yaml).
31 changes: 31 additions & 0 deletions bootstrap-pull-request/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: bootstrap-pull-request
description: bootstrap the pull request namespace

inputs:
overlay:
description: Name of overlay
required: true
namespace:
description: Name of namespace
required: true
source-repository:
description: Source repository
required: true
default: ${{ github.repository }}
destination-repository:
description: Destination repository
required: true
destination-repository-token:
description: GitHub token for destination repository
required: true
default: ${{ github.token }}
namespace-manifest:
description: Path to namespace manifest (optional)
required: false
substitute-variables:
description: Pairs of key=value to substitute the prebuilt manifests (multiline)
required: false

runs:
using: 'node20'
main: 'dist/index.js'
7 changes: 7 additions & 0 deletions bootstrap-pull-request/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
clearMocks: true,
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
verbose: true,
}
15 changes: 15 additions & 0 deletions bootstrap-pull-request/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "bootstrap-pull-request",
"version": "0.0.0",
"private": true,
"main": "lib/src/main.js",
"scripts": {
"build": "tsc",
"package": "ncc build --source-map --license licenses.txt",
"test": "jest",
"ci:package": "yarn build && yarn package"
},
"dependencies": {
"@actions/core": "1.10.1"
}
}
80 changes: 80 additions & 0 deletions bootstrap-pull-request/src/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as os from 'os'
import * as path from 'path'
import { promises as fs } from 'fs'

type CheckoutOptions = {
repository: string
branch: string
token: string
}

export const checkout = async (opts: CheckoutOptions) => {
const cwd = await fs.mkdtemp(path.join(process.env.RUNNER_TEMP || os.tmpdir(), 'git-'))
core.info(`Cloning ${opts.repository} into ${cwd}`)
await exec.exec('git', ['version'], { cwd })
await exec.exec('git', ['init', '--initial-branch', opts.branch], { cwd })
await exec.exec('git', ['config', '--local', 'gc.auto', '0'], { cwd })
await exec.exec('git', ['remote', 'add', 'origin', `https://github.com/${opts.repository}`], { cwd })
const credentials = Buffer.from(`x-access-token:${opts.token}`).toString('base64')
core.setSecret(credentials)
await exec.exec(
'git',
['config', '--local', 'http.https://github.com/.extraheader', `AUTHORIZATION: basic ${credentials}`],
{ cwd },
)
await exec.exec(
'git',
['fetch', '--no-tags', '--depth=1', 'origin', `+refs/heads/${opts.branch}:refs/remotes/origin/${opts.branch}`],
{ cwd },
)
await exec.exec('git', ['checkout', opts.branch], { cwd })
return cwd
}

export const checkoutOrInitRepository = async (opts: CheckoutOptions) => {
const cwd = await fs.mkdtemp(path.join(process.env.RUNNER_TEMP || os.tmpdir(), 'git-'))
core.info(`Cloning ${opts.repository} into ${cwd}`)
await exec.exec('git', ['version'], { cwd })
await exec.exec('git', ['init', '--initial-branch', opts.branch], { cwd })
await exec.exec('git', ['config', '--local', 'gc.auto', '0'], { cwd })
await exec.exec('git', ['remote', 'add', 'origin', `https://github.com/${opts.repository}`], { cwd })
const credentials = Buffer.from(`x-access-token:${opts.token}`).toString('base64')
core.setSecret(credentials)
await exec.exec(
'git',
['config', '--local', 'http.https://github.com/.extraheader', `AUTHORIZATION: basic ${credentials}`],
{ cwd },
)
const code = await exec.exec(
'git',
['fetch', '--no-tags', '--depth=1', 'origin', `+refs/heads/${opts.branch}:refs/remotes/origin/${opts.branch}`],
{ cwd, ignoreReturnCode: true },
)
if (code === 0) {
await exec.exec('git', ['checkout', opts.branch], { cwd })
return cwd
}
// If the remote branch does not exist, set up the tracking branch.
await exec.exec('git', ['config', '--local', `branch.${opts.branch}.remote`, 'origin'], { cwd })
await exec.exec('git', ['config', '--local', `branch.${opts.branch}.merge`, `refs/heads/${opts.branch}`], { cwd })
return cwd
}

export const status = async (cwd: string): Promise<string> => {
const output = await exec.getExecOutput('git', ['status', '--porcelain'], { cwd })
return output.stdout.trim()
}

export const commit = async (cwd: string, message: string): Promise<void> => {
await exec.exec('git', ['add', '.'], { cwd })
await exec.exec('git', ['config', 'user.email', '41898282+github-actions[bot]@users.noreply.github.com'], { cwd })
await exec.exec('git', ['config', 'user.name', 'github-actions[bot]'], { cwd })
await exec.exec('git', ['commit', '-m', message], { cwd })
await exec.exec('git', ['rev-parse', 'HEAD'], { cwd })
}

export const pushByFastForward = async (cwd: string): Promise<number> => {
return await exec.exec('git', ['push', 'origin'], { cwd, ignoreReturnCode: true })
}
19 changes: 19 additions & 0 deletions bootstrap-pull-request/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as core from '@actions/core'
import { run } from './run'

const main = async (): Promise<void> => {
await run({
overlay: core.getInput('overlay', { required: true }),
namespace: core.getInput('namespace', { required: true }),
sourceRepository: core.getInput('source-repository', { required: true }),
destinationRepository: core.getInput('destination-repository', { required: true }),
destinationRepositoryToken: core.getInput('destination-repository-token', { required: true }),
namespaceManifest: core.getInput('namespace-manifest') || undefined,
substituteVariables: core.getMultilineInput('substitute-variables'),
})
}

main().catch((e: Error) => {
core.setFailed(e)
console.error(e)
})
Loading

0 comments on commit f6b63df

Please sign in to comment.