diff --git a/deployment-status/Dockerfile b/deployment-status/Dockerfile deleted file mode 100644 index 8c2d1ab..0000000 --- a/deployment-status/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3.10-slim - -COPY ./app/ /app - -RUN cd /app && pip3 install -r requirements.txt --no-cache-dir - -RUN useradd -u 1021 -m app - -USER app - -WORKDIR /app - -ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/deployment-status/README.md b/deployment-status/README.md index ede56a9..2f4f4be 100644 --- a/deployment-status/README.md +++ b/deployment-status/README.md @@ -1,15 +1,11 @@ # GitHub Deployment/Release Script -This action is used to manage github deployments and releases on a target repository +This action is used to set the deployment status for GitHub deployments on a different repository. ex. https://github.com/mozilla/fxa/deployments & https://github.com/mozilla/fxa/releases ## Inputs -### `command` - -The command to run. Supported values delete-deployment, delete-release, get-all-deployments, get-all-releases, update-deployment, update-release. Default `"update-deployment"` - ### `github_org` The GitHub organization hosting the target repository. Default `"mozilla"` @@ -20,19 +16,27 @@ The GitHub organization hosting the target repository. Default `"mozilla"` ### `environment_url` -The environment URL to set in the deployment request. +The environment URL to set in the deployment request. ### `state` -The deployment state. Supported values in_progress, success, failed. Default `"in_progress"` +The deployment state. Supported values in_progress, success, failure. Default `"in_progress"` + +### `deployment_id` + +The id of the pre-existing GitHub deployment to set the status for. -### `environment` +### `environment`, `ref`, `sha` -The target deployment environment. Default `"staging"` +Used to uniquely identify the deployment to update, as an alternative to `deployment_id`. Will be ignored if `deployment_id` is set. If no deployment with the given settings exists, a new deployment will be created using this information. -### `ref` +### `app_id` -**Required** The git ref being deployed from the target repository. +The id of the GitHub app with the permission to update deployment statuses on the target repo. + +### `private_key` + +The private key of the GitHub app. ### Example usage ``` @@ -48,31 +52,26 @@ on: description: "Target deployment environment" type: string default: "staging" - ref: + ref: description: "Deployed git ref" required: true default: "v1.269.2" -env: - APPLICATION_ID: ${{ secrets.APPLICATION_ID }} - INSTALLATION_ID: ${{ secrets.INSTALLATION_ID }} - PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} - jobs: update_deployment_job: - permissions: - contents: read - id-token: write runs-on: ubuntu-latest steps: - name: Set repository deployment status uses: mozilla-it/deploy-actions/deployment-status@v4.0.0 with: + github_org: mozilla repository: fxa environment_url: "https://accounts.stage.mozaws.net/__version__" - state: ${{ github.event.inputs.state }} - environment: ${{ github.event.inputs.environment }} - ref: ${{ github.event.inputs.ref }} + state: ${{ inputs.state }} + environment: ${{ inputs.environment }} + ref: ${{ inputs.ref }} + app_id: ${{ secrets.APPLICATION_ID }} + private_key: ${{ secrets.PRIVATE_KEY }} ``` @@ -82,6 +81,4 @@ A GitHub app with the permissions to create/modify deployments and/or releases o - `APPLICATION_ID` - The application ID of the required GitHub app -- `INSTALLATION_ID` - The installation ID of the GitHub app installed in the target repository - -- `PRIVATE_KEY` - The private key of the required GitHub app \ No newline at end of file +- `PRIVATE_KEY` - The private key of the required GitHub app diff --git a/deployment-status/action.yml b/deployment-status/action.yml index 6f137f3..2bd99b4 100644 --- a/deployment-status/action.yml +++ b/deployment-status/action.yml @@ -1,59 +1,90 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - name: Deployment status -description: Creates and manages GitHub deployments and releases on target repositories +description: Sets the deployment status for a deployment on a different repository inputs: - command: - description: Command to run. Supported values delete-deployment, delete-release, get-all-deployments, get-all-releases, update-deployment, update-release - required: false - type: string - default: update-deployment github_org: description: GitHub organization hosting the target repository - required: false type: string - default: mozilla + required: true repository: description: GitHub repository to target type: string required: true - environment_url: - description: Environment URL to set in deployment + deployment_id: + description: The id of the dpeloyment we want to set the status for type: string - required: false - default: "" - state: - description: State of deployment. Supported values in_progress, success, failed - type: choice - default: in_progress - environment: - description: Target deployment environment + required: false + sha: + description: Deployed git sha type: string - default: staging - ref: + required: false + ref: description: Deployed git ref type: string + required: false + environment: + description: Target deployment environment + type: string + required: false + state: + description: State of deployment. Supported values in_progress, success, failure + type: choice required: true + environment_url: + description: Environment URL to set in deployment + type: string + required: false + app_id: + description: The id of the GitHub app to set the deployment status + type: string + required: false + private_key: + description: The private key of the deployment status app + type: string + required: false runs: - using: docker - image: Dockerfile - args: - - -r - - ${{ inputs.repository }} - - -o - - ${{ inputs.github_org }} - - ${{ inputs.command }} - - --tag - - ${{ inputs.ref }} - - --environment - - ${{ inputs.environment }} - - --state - - ${{ inputs.state }} - - --environment_url - - ${{ inputs.environment_url }} + using: "composite" + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: "${{ inputs.app_id || env.APPLICATION_ID }}" + private-key: "${{ inputs.private_key || env.PRIVATE_KEY }}" + owner: "${{ inputs.github_org }}" + repositories: "${{ inputs.repository }}" + - name: Set deployment status + uses: actions/github-script@v7 + with: + github-token: "${{ steps.app-token.outputs.token }}" + script: | + const inputs = ${{ toJSON(inputs) }}; + let deployment_id = inputs.deployment_id; + if (!deployment_id) { + deployment_data = { + owner: inputs.github_org, + repo: inputs.repository, + sha: inputs.sha, + ref: inputs.ref, + environment: inputs.environment, + } + const deployments = (await github.rest.repos.listDeployments(deployment_data)).data; + if (deployments.length) { + deployment_id = deployments[0].id; + } else { + deployment_data.auto_merge = false; + deployment_data.required_contexts = []; + const deployment = (await github.rest.repos.createDeployment(deployment_data)).data; + deployment_id = deployment.id; + } + } + github.rest.repos.createDeploymentStatus({ + owner: inputs.github_org, + repo: inputs.repository, + deployment_id, + state: inputs.state, + log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + environment_url: inputs.environment_url, + }) diff --git a/deployment-status/app/entrypoint.sh b/deployment-status/app/entrypoint.sh deleted file mode 100755 index 0778bf2..0000000 --- a/deployment-status/app/entrypoint.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -l - -/app/main.py $@ diff --git a/deployment-status/app/main.py b/deployment-status/app/main.py deleted file mode 100755 index 41cd3b5..0000000 --- a/deployment-status/app/main.py +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/env python3 - -import click, jwt, requests, time, json, os -from cryptography.hazmat.backends import default_backend - - -@click.group() -@click.option( - "--key", - "-k", - help="Private key of github app", - envvar="PRIVATE_KEY", - default="/app/key.pem", - show_default=True, -) -@click.option( - "--repo", - "-r", - help="Github repository being deployed", - default="fxa", - show_default=True, -) -@click.option( - "--organization", - "-o", - help="Github organization containing repository", - default="mozilla", - show_default=True, -) -@click.option( - "--app_id", - help="Application ID of Github App with deployment and release permissions in desired repository", - envvar="APPLICATION_ID", - required=True, -) -@click.option( - "--install_id", - help="Installation ID of Github App installed in desired repository", - envvar="INSTALLATION_ID", - required=True, -) -def main(key, repo, organization, app_id, install_id): - private_pem = key.encode() - main.private_key = default_backend().load_pem_private_key(private_pem, None) - main.gh_repo = repo - main.gh_org = organization - main.gh_app_id = app_id - main.installation_id = install_id - pass - - -def get_token(): - payload = { - # issued at time, 60 seconds in the past to allow for clock drift - "iat": int(time.time()), - # JWT expiration time (10 minute maximum) - "exp": int(time.time()) + (10 * 60), - # GitHub App identifier - "iss": main.gh_app_id, - } - - bearer = jwt.encode(payload, main.private_key, algorithm="RS256") - headers = { - "Authorization": f"Bearer {bearer}", - "Accept": "application/vnd.github.machine-man-preview+json", - "Content-Type": "application/json", - } - - resp = requests.post( - f"https://api.github.com/app/installations/{main.installation_id}/access_tokens", - headers=headers, - ) - content_json = json.loads(resp.content.decode()) - - return content_json["token"] - - -def get_headers(): - headers = { - "Authorization": f"token {get_token()}", - "Accept": "application/vnd.github.machine-man-preview+json", - "Content-Type": "application/json", - } - return headers - - -def create_release(tag): - data = f'{{"tag_name": "{tag}", "name": "{tag}", "prerelease": true, "generate_release_notes": true}}' - resp = requests.post( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/releases", - headers=get_headers(), - data=data, - ) - - content_json = json.loads(resp.content.decode()) - return content_json - - -def get_release(tag): - resp = requests.get( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/releases/tags/{tag}", - headers=get_headers(), - ) - content_json = json.loads(resp.content.decode()) - if resp.status_code == 404: - return False - else: - return content_json["id"] - - -@click.command() -@click.option( - "--environment", - "-e", - default="staging", - help="Deployment environment: (development|staging|production)", - show_default=True, -) -@click.option( - "--tag", "-t", help="Git tag being deployed: (1.224.1|1.228.0)", required=True -) -def update_release(tag, environment): - release_id = get_release(tag) - - if not release_id: - tag_content = create_release(tag) - print(tag_content["html_url"]) - return tag_content - - if environment == "production": - data = '{"prerelease": false}' - else: - data = '{"prerelease": true}' - - resp = requests.patch( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/releases/{release_id}", - headers=get_headers(), - data=data, - ) - content_json = json.loads(resp.content.decode()) - print(content_json["html_url"]) - return resp.status_code - - -@click.command() -@click.option("--release-id", help="Release ID", required=True) -def delete_release(release_id): - resp = requests.delete( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/releases/{release_id}", - headers=get_headers(), - ) - return resp.status_code - - -def create_deployment(tag, environment): - data = f'{{"ref": "{tag}", "environment": "{environment}", "task":"deploy", "auto_merge": false, "required_contexts": [] }}' - - resp = requests.post( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/deployments", - headers=get_headers(), - data=data, - ) - content_json = json.loads(resp.content.decode()) - if resp.status_code == 200: - return content_json["id"] - else: - return content_json - - -def get_deployment(tag, environment): - resp = requests.get( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/deployments", - headers=get_headers(), - ) - content_json = json.loads(resp.content.decode()) - try: - matching_deployment = next( - deployment - for deployment in content_json - if (deployment["environment"] == environment and deployment["ref"] == tag) - ) - deployment_id = matching_deployment["id"] - except StopIteration: - deployment_id = "" - - return deployment_id - - -@click.command() -@click.option( - "--tag", "-t", help="Git tag being deployed: (1.224.1|1.228.0)", required=True -) -@click.option( - "--environment", - "-e", - default="staging", - help="Deployment environment: (staging|production)", - show_default=True, -) -@click.option( - "--state", - "-s", - default="in_progress", - help="Deployment state: (in_progress|success|failed)", - show_default=True, -) -@click.option( - "--environment_url", - "-u", - default="https://mozilla.org", - help="Environment url: ex. (https://accounts.firefox.com)", - show_default=False, -) -def update_deployment(tag, environment, environment_url, state="in_progress"): - deployment_id = get_deployment(tag, environment) - - if not deployment_id: - create_deployment(tag, environment) - deployment_id = get_deployment(tag, environment) - - data = f'{{"environment": "{environment}", "state": "{state}", "environment_url": "{environment_url}" }}' - - resp = requests.post( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/deployments/{deployment_id}/statuses", - headers=get_headers(), - data=data, - ) - content_json = json.loads(resp.content.decode()) - print(content_json) - return resp.status_code - - -@click.command() -def get_all_releases(): - resp = requests.get( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/releases", - headers=get_headers(), - ) - content_json = json.loads(resp.content.decode()) - print(f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/releases") - print(content_json) - - -@click.command() -def get_all_deployments(): - resp = requests.get( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/deployments", - headers=get_headers(), - ) - content_json = json.loads(resp.content.decode()) - print(content_json) - - -@click.command() -@click.option("--deployment-id", help="Deployment ID", required=True) -def delete_deployment(deployment_id): - resp = requests.delete( - f"https://api.github.com/repos/{main.gh_org}/{main.gh_repo}/deployments/{deployment_id}", - headers=get_headers(), - ) - - return resp.status_code - - -# Adding commands to main -main.add_command(update_deployment) -main.add_command(update_release) -main.add_command(get_all_deployments) -main.add_command(get_all_releases) -main.add_command(delete_deployment) -main.add_command(delete_release) - - -if __name__ == "__main__": - main() diff --git a/deployment-status/app/requirements.txt b/deployment-status/app/requirements.txt deleted file mode 100644 index df3f6fa..0000000 --- a/deployment-status/app/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -click==7.1.2 -requests==2.25.0 -PyJWT==2.3.0 -cryptography==3.4.7