Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/environment-outputs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: environment-outputs

on:
pull_request:
paths:
- environment-outputs/**
- '*.json'
- '*.yaml'
- .github/workflows/environment-outputs.yaml
push:
branches:
- main
paths:
- environment-outputs/**
- '*.json'
- '*.yaml'
- .github/workflows/environment-outputs.yaml

defaults:
run:
working-directory: environment-outputs

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
- run: corepack enable pnpm
- run: pnpm i
- run: pnpm test

e2e-test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
- run: corepack enable pnpm
- run: pnpm i
- run: pnpm build
- uses: ./environment-outputs
id: environment
with:
service: example
rules: |
- pull_request:
base: '**'
head: '**'
outputs:
overlay: pr
namespace: pr-${{ github.event.pull_request.number }}
- push:
ref: refs/heads/main
outputs:
overlay: development
namespace: development
- run: echo 'overlay=${{ steps.environment.outputs.overlay }}'
- run: echo 'namespace=${{ steps.environment.outputs.namespace }}'
- run: echo 'github-deployment-url=${{ steps.environment.outputs.github-deployment-url }}'
146 changes: 146 additions & 0 deletions environment-outputs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# environment-outputs [![environment-outputs](https://github.com/quipper/monorepo-deploy-actions/actions/workflows/environment-outputs.yaml/badge.svg)](https://github.com/quipper/monorepo-deploy-actions/actions/workflows/environment-outputs.yaml)

This action generates outputs to deploy a service to the corresponding environment.

## Getting Started

Let's think about the following example:

- When a pull request is created, deploy it to `pr-NUMBER` namespace
- When `main` branch is pushed, deploy it to `development` namespace

It can be descibed as the following rules:

```yaml
- pull_request:
base: '**'
head: '**'
outputs:
overlay: pr
namespace: pr-${{ github.event.pull_request.number }}
- push:
ref: refs/heads/main
outputs:
overlay: development
namespace: development
```

This action finds a rule matched to the current context.
If any rule is matched, this action returns the outputs corresponding to the rule.
For example, when `main` branch is pushed, this action returns the following outputs:

```yaml
overlay: development
namespace: development
```

This action finds a rule in order.
If no rule is matched, this action fails.

## GitHub Deployment

This action supports [GitHub Deployment](https://docs.github.com/en/rest/deployments/deployments) to receive the deployment status from an external system, such as Argo CD.

It creates a GitHub Deployment for each environment in the form of `{overlay}/{namespace}/{service}`,
if the following fields are given:

- `overlay` (in `environments`)
- `namespace` (in `environments`)
- `service` (in the inputs)

If an old deployment exists, this action deletes it and recreates new one.

This action sets `github-deployment-url` field to the output.
For example, the below inputs are given,

```yaml
- uses: quipper/monorepo-deploy-actions/environment-outputs@v1
with:
service: backend
rules: |
- pull_request:
base: '**'
head: '**'
outputs:
overlay: pr
namespace: pr-${{ github.event.pull_request.number }}
```

this action creates a GitHub Deployment of `pr/pr-1/backend` and returns the following outputs:

```yaml
overlay: pr
namespace: pr-1
github-deployment-url: https://api.github.com/repos/octocat/example/deployments/1
```

## Example

Here is the example workflow.

```yaml
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: quipper/monorepo-deploy-actions/environment-outputs@v1
id: environment
with:
service: example
rules: |
- pull_request:
base: '**'
head: '**'
outputs:
overlay: pr
namespace: pr-${{ github.event.pull_request.number }}
- push:
ref: refs/heads/main
outputs:
overlay: development
namespace: development
- uses: quipper/monorepo-deploy-actions/git-push-service@v1
with:
manifests: # (omit in this example)
overlay: ${{ steps.environment.outputs.overlay }}
namespace: ${{ steps.environment.outputs.namespace }}
service: example
application-annotations: |
argocd-commenter.int128.github.io/deployment-url=${{ steps.environment.outputs.github-deployment-url }}
```

## Spec

### Inputs

| Name | Default | Description |
| --------- | -------------- | ----------------------------------------------------------- |
| `rules` | (required) | YAML string of rules |
| `service` | (optional) | Name of service to deploy. If set, create GitHub Deployment |
| `token` | `github.token` | GitHub token, required if `service` is set |

The following fields are available in the rules YAML.

```yaml
- pull_request: # on pull_request event
base: # base branch name (wildcard available)
head: # head branch name (wildcard available)
outputs: # map<string, string>
- push: # on push event
ref: refs/heads/main # ref name (wildcard available)
outputs: # map<string, string>
```

It supports the wildcard pattern.
See https://github.com/isaacs/minimatch for details.

### Outputs

This actions returns the outputs corresponding to the rule.

It also returns the below outputs.

| Name | Description |
| ----------------------- | --------------------------------------------------------------------- |
| `github-deployment-url` | URL of GitHub Deployment. Available if `service` is set in the inputs |
19 changes: 19 additions & 0 deletions environment-outputs/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: environment-matrix
description: generate a JSON for matrix deploy
inputs:
rules:
description: YAML string of rules
required: true
service:
description: Name of service. If set, create GitHub Deployment
required: false
token:
description: GitHub token, required if service is set
required: false
default: ${{ github.token }}
outputs:
github-deployment-url:
description: URL of the GitHub Deployment, e.g. https://api.github.com/repos/octocat/example/deployments/1
runs:
using: 'node20'
main: 'dist/index.js'
7 changes: 7 additions & 0 deletions environment-outputs/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,
}
22 changes: 22 additions & 0 deletions environment-outputs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "environment-outputs",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "ncc build --source-map --license licenses.txt src/main.ts",
"test": "jest"
},
"dependencies": {
"@actions/core": "1.10.1",
"@actions/github": "6.0.0",
"@octokit/plugin-retry": "6.0.1",
"@octokit/request-error": "5.0.1",
"ajv": "8.12.0",
"js-yaml": "4.1.0",
"minimatch": "9.0.3"
},
"devDependencies": {
"@types/js-yaml": "4.0.9",
"@types/minimatch": "5.1.2"
}
}
82 changes: 82 additions & 0 deletions environment-outputs/src/deployment.ts
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--- a/environment-matrix/src/deployment.ts
+++ b/environment-outputs/src/deployment.ts
@@ -1,39 +1,12 @@
 import * as core from '@actions/core'
 import * as github from '@actions/github'
-import { Environment } from './rule'
 import { RequestError } from '@octokit/request-error'
 import { Octokit, assertPullRequestPayload } from './github'
 import assert from 'assert'

 type Context = Pick<typeof github.context, 'eventName' | 'repo' | 'ref' | 'payload'>

-export type EnvironmentWithDeployment = Environment & {
-  // URL of the GitHub Deployment
-  // e.g. https://api.github.com/repos/octocat/example/deployments/1
-  'github-deployment-url': string
-}
-
-export const createGitHubDeploymentForEnvironments = async (
-  octokit: Octokit,
-  context: Context,
-  environments: Environment[],
-  service: string,
-): Promise<EnvironmentWithDeployment[]> => {
-  const environmentsWithDeployments = []
-  for (const environment of environments) {
-    const { overlay, namespace } = environment
-    if (overlay && namespace && service) {
-      const deployment = await createDeployment(octokit, context, overlay, namespace, service)
-      environmentsWithDeployments.push({
-        ...environment,
-        'github-deployment-url': deployment.url,
-      })
-    }
-  }
-  return environmentsWithDeployments
-}
-
-const createDeployment = async (
+export const createDeployment = async (
   octokit: Octokit,
   context: Context,
   overlay: string,
@@ -96,7 +69,7 @@ const createDeployment = async (
     state: 'inactive',
   })
   core.info(`Set the deployment status to inactive`)
-  return created.data
+  return created.data.url
 }

 const getDeploymentRef = (context: Context): string => {

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import { RequestError } from '@octokit/request-error'
import { Octokit, assertPullRequestPayload } from './github'
import assert from 'assert'

type Context = Pick<typeof github.context, 'eventName' | 'repo' | 'ref' | 'payload'>

export const createDeployment = async (
octokit: Octokit,
context: Context,
overlay: string,
namespace: string,
service: string,
) => {
const environment = `${overlay}/${namespace}/${service}`

core.info(`Finding the old deployments for environment ${environment}`)
const oldDeployments = await octokit.rest.repos.listDeployments({
owner: context.repo.owner,
repo: context.repo.repo,
environment,
})

core.info(`Deleting ${oldDeployments.data.length} deployment(s)`)
for (const deployment of oldDeployments.data) {
try {
await octokit.rest.repos.deleteDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.id,
})
} catch (error) {
if (error instanceof RequestError) {
core.warning(`Could not delete the old deployment ${deployment.url}: ${error.status} ${error.message}`)
continue
}
throw error
}
core.info(`Deleted the old deployment ${deployment.url}`)
}
core.info(`Deleted ${oldDeployments.data.length} deployment(s)`)

const ref = getDeploymentRef(context)
core.info(`Creating a deployment for environment=${environment}, ref=${ref}`)
const created = await octokit.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref,
environment,
auto_merge: false,
required_contexts: [],
transient_environment: context.eventName === 'pull_request',
payload: { overlay, namespace, service },
})
assert.strictEqual(created.status, 201)
core.info(`Created a deployment ${created.data.url}`)

// If the deployment is not deployed for a while, it will cause the following error:
// This branch had an error being deployed
// 1 abandoned deployment
//
// To avoid this, we set the deployment status to inactive immediately.
core.info(`Setting the deployment status to inactive`)
await octokit.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: created.data.id,
state: 'inactive',
})
core.info(`Set the deployment status to inactive`)
return created.data.url
}

const getDeploymentRef = (context: Context): string => {
if (context.eventName === 'pull_request') {
// Set the head ref to associate a deployment with the pull request
assertPullRequestPayload(context.payload.pull_request)
return context.payload.pull_request.head.ref
}
return context.ref
}
36 changes: 36 additions & 0 deletions environment-outputs/src/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from 'assert'
import * as github from '@actions/github'
import * as pluginRetry from '@octokit/plugin-retry'

export type Octokit = ReturnType<typeof github.getOctokit>

export const getOctokit = (token: string): Octokit => {
return github.getOctokit(token, { previews: ['ant-man', 'flash'] }, pluginRetry.retry)
}

// picked from https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request
export type PullRequestPayload = {
head: {
ref: string
}
base: {
ref: string
}
}

export function assertPullRequestPayload(x: unknown): asserts x is PullRequestPayload {
assert(typeof x === 'object')
assert(x != null)

assert('base' in x)
assert(typeof x.base === 'object')
assert(x.base != null)
assert('ref' in x.base)
assert(typeof x.base.ref === 'string')

assert('head' in x)
assert(typeof x.head === 'object')
assert(x.head != null)
assert('ref' in x.head)
assert(typeof x.head.ref === 'string')
}
Loading