generated from int128/typescript-actions-monorepo
-
Notifications
You must be signed in to change notification settings - Fork 6
Add environment-outputs action #1336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
int128
wants to merge
4
commits into
main
Choose a base branch
from
int128/environment-outputs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # environment-outputs [](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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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') | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.