diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..abb32ad --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,120 @@ +name: Test workflow for developing the frizbee-action +on: + push: + workflow_dispatch: + +permissions: write-all + +jobs: + test_pr_and_fail_defaults: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: ./ # Uses an action in the root directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + open_pr: true + fail_on_unpinned: true + test_pr_and_fail: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: ./ # Uses an action in the root directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + actions: tests/workflows + dockerfiles: tests/dockerfiles + kubernetes: tests/k8s + docker_compose: tests/docker_compose + open_pr: true + fail_on_unpinned: true + test_pr_and_fail_again: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: ./ # Uses an action in the root directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + actions: tests/workflows + dockerfiles: tests/dockerfiles + kubernetes: tests/k8s + docker_compose: tests/docker_compose + open_pr: true + fail_on_unpinned: true + test_no_pr_and_fail: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: ./ # Uses an action in the root directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + actions: tests/workflows + dockerfiles: tests/dockerfiles + kubernetes: tests/k8s + docker_compose: tests/docker_compose + open_pr: false + fail_on_unpinned: true + test_no_pr_and_no_fail: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: ./ # Uses an action in the root directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + actions: tests/workflows + dockerfiles: tests/dockerfiles + kubernetes: tests/k8s + docker_compose: tests/docker_compose + open_pr: false + fail_on_unpinned: false + test_no_pr_and_no_fail_no_actions: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: ./ # Uses an action in the root directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + actions: "" + dockerfiles: "." + kubernetes: "" + docker_compose: "" + open_pr: false + fail_on_unpinned: false + test_no_pr_and_no_fail_default: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: ./ # Uses an action in the root directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + open_pr: false + fail_on_unpinned: false + test_no_pr_and_no_fail_conflict: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: ./ # Uses an action in the root directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + actions: "" + dockerfiles: "." + kubernetes: "." + docker_compose: "." + open_pr: false + fail_on_unpinned: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index c70bd7a..75755da 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ Thumbs.db # asdf .tool-versions + +frizbee-action \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a465734 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ + +# Contributing to Frizbee Action +First off, thank you for taking the time to contribute to Frizbee Action! :+1: :tada: Frizbee Action is released under the Apache 2.0 license. If you would like to contribute something or want to hack on the code, this document should help you get started. You can find some hints for starting development in Frizbee's [README](https://github.com/stacklok/frizbee-action/blob/main/README.md). + +## Table of contents +- [Code of Conduct](#code-of-conduct) +- [Reporting Security Vulnerabilities](#reporting-security-vulnerabilities) +- [How to Contribute](#how-to-contribute) + - [Sign the Contributor License Agreement](#sign-the-contributor-license-agreement) + - [Using GitHub Issues](#using-github-issues) + - [Not sure how to start contributing...](#not-sure-how-to-start-contributing) + - [Pull Request Process](#pull-request-process) + - [Contributing to docs](#contributing-to-docs) + - [Commit Message Guidelines](#commit-message-guidelines) + + +## Code of Conduct +This project adheres to the [Contributor Covenant](https://github.com/stacklok/frizbee/blob/main/CODE_OF_CONDUCT.md) code of conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to code-of-conduct@stacklok.dev. + +## Reporting Security Vulnerabilities + +If you think you have found a security vulnerability in Frizbee please DO NOT disclose it publicly until we’ve had a chance to fix it. Please don’t report security vulnerabilities using GitHub issues; instead, please follow this [process](https://github.com/stacklok/frizbee/blob/main/SECURITY.md) + +## How to Contribute + +### Using GitHub Issues +We use GitHub issues to track bugs and enhancements. If you have a general usage question, please ask in [Frizbee's discussion forum](https://discord.com/invite/RkzVuTp3WK). + +If you are reporting a bug, please help to speed up problem diagnosis by providing as much information as possible. Ideally, that would include a small sample project that reproduces the problem. + +### Sign the Contributor License Agreement +Before we accept a non-trivial patch or pull request, we will need you to sign the [Contributor License Agreement](https://github.com/stacklok/frizbee). Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Active contributors might be asked to join the core team and given the ability to merge pull requests. + +### Not sure how to start contributing... +PRs to resolve existing issues are greatly appreciated and issues labeled as ["good first issue"](https://github.com/stacklok/frizbee/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) are a great place to start! + +### Pull Request Process +* Create an issue outlining the fix or feature. +* Fork the Frizbee Action repository to your own GitHub account and clone it locally. +* Hack on your changes. +* Correctly format your commit messages, see [Commit Message Guidelines](#Commit-Message-Guidelines) below. +* Open a PR by ensuring the title and its description reflect the content of the PR. +* Ensure that CI passes, if it fails, fix the failures. +* Every pull request requires a review from the core Frizbee Action team before merging. +* Once approved, all of your commits will be squashed into a single commit with your PR title. + +### Commit Message Guidelines +We follow the commit formatting recommendations found on [Chris Beams' How to Write a Git Commit Message article](https://chris.beams.io/posts/git-commit/). diff --git a/Dockerfile b/Dockerfile index 3f3a889..d07d68b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ FROM golang:alpine3.19@sha256:0466223b8544fb7d4ff04748acc4d75a608234bf4e79563bff208d2060c0dd79 -RUN apk add git COPY . /home/src WORKDIR /home/src diff --git a/README.md b/README.md index 2382e03..9e09fbc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,96 @@ +![image](https://github.com/stacklok/frizbee/assets/16540482/35034046-d962-475d-b8e2-67b7625f2a60) + +--- +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache2.0-brightgreen.svg)](https://opensource.org/licenses/Apache-2.0) | [![](https://dcbadge.vercel.app/api/server/RkzVuTp3WK?logo=discord&label=Discord&color=5865&style=flat)](https://discord.gg/RkzVuTp3WK) + +--- # Frizbee Action +Frizbee Action helps you pin your GitHub Actions and container images to specific versions using checksums. + +You can configure it to fix it all for you and open a PR with the proposed changes, +fail the CI if unpinned actions are found and much more. + +The action is based on the Frizbee tool, available both as a CLI and as a library - https://github.com/stacklok/frizbee + +## Table of Contents + +- [Usage](#usage) +- [Configuration](#configuration) +- [Contributing](#contributing) +- [License](#license) + +## Usage + +To use the Frizbee Action, you can use the following methods: + +```bash +name: Frizbee Pinned Actions and Container Images Check + +on: + schedule: + - cron: '0 0 * * *' # Run every day at midnight + workflow_dispatch: + +jobs: + frizbee_check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: stacklok/frizbee-action@v0.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + actions: .github/workflows + dockerfiles: ./docker + kubernetes: ./k8s + docker_compose: ./docker + open_pr: true + fail_on_unpinned: true +``` + +## Configuration + +The Frizbee Action can be configured through the following inputs: + +```yml + actions: + description: "Actions to correct" + required: false + default: ".github/workflows" + dockerfiles: + description: "Dockerfiles to correct" + required: false + default: "Dockerfile" + kubernetes: + description: "Kubernetes manifests to correct" + required: false + default: "" + docker_compose: + description: "Docker Compose files to correct" + required: false + default: "" + open_pr: + description: "Open a PR with the changes" + required: false + default: "true" + fail_on_unpinned: + description: "Fail if an unpinned action/image is found" + required: false + default: "false" +``` + +### Limitations + +The default `GITHUB_TOKEN` doesn't have the necessary permissions (`workflows`) to open a PR. +In case you want to use the `open_pr` feature, you will need to create a new token with the correct scope, add it as a secret +and pass it to the action through the `GITHUB_TOKEN` environment variable. + +## Contributing + +We welcome contributions to Frizbee Action. Please see our [Contributing](./CONTRIBUTING.md) guide for more information. + +## License + +Frizbee is licensed under the [Apache 2.0 License](./LICENSE). \ No newline at end of file diff --git a/action.yml b/action.yml index dfbd2ce..0e04ead 100644 --- a/action.yml +++ b/action.yml @@ -5,10 +5,30 @@ branding: icon: "at-sign" color: "green" inputs: - GITHUB_TOKEN: - description: "GitHub token" - required: true - + actions: + description: "Actions to correct" + required: false + default: ".github/workflows" + dockerfiles: + description: "Dockerfiles to correct" + required: false + default: "Dockerfile" + kubernetes: + description: "Kubernetes manifests to correct" + required: false + default: "" + docker_compose: + description: "Docker Compose files to correct" + required: false + default: "" + open_pr: + description: "Open a PR with the changes" + required: false + default: "true" + fail_on_unpinned: + description: "Fail if an unpinned action/image is found" + required: false + default: "false" runs: using: "docker" image: "Dockerfile" diff --git a/go.mod b/go.mod index f4bf57a..16ed180 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,54 @@ module github.com/stacklok/frizbee-action go 1.22.1 require ( + github.com/deckarep/golang-set/v2 v2.6.0 + github.com/go-git/go-billy/v5 v5.5.0 + github.com/go-git/go-git/v5 v5.12.0 github.com/google/go-github/v60 v60.0.0 + github.com/stacklok/frizbee v0.0.19 golang.org/x/oauth2 v0.21.0 ) -require github.com/google/go-querystring v1.1.0 // indirect +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.9+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-containerregistry v0.19.1 // indirect + github.com/google/go-github/v61 v61.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.16.5 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc3 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/puzpuzpuz/xsync v1.5.2 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.1 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/tools v0.13.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 452c7cf..b2f5f23 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,211 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= +github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8= github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk= +github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= +github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY= +github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ= +github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stacklok/frizbee v0.0.19 h1:lD5O5e1lCYl6yGTtWW93m2w60TMeTJB5oLXMeaHnFHo= +github.com/stacklok/frizbee v0.0.19/go.mod h1:Hvi3/ryonTgeMBG4/EtBGjfK49W0rP0P3+0RAg3kqHI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/main.go b/main.go index 0d5826d..ba71277 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,108 @@ package main +import ( + "context" + "errors" + "fmt" + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/storage/memory" + "github.com/google/go-github/v60/github" + "github.com/stacklok/frizbee-action/pkg/action" + "github.com/stacklok/frizbee/pkg/replacer" + "github.com/stacklok/frizbee/pkg/utils/config" + + "golang.org/x/oauth2" + "log" + "os" + "strings" +) + func main() { - // This is a placeholder for the main function + ctx := context.Background() + // Initialize the frizbee action + frizbeeAction, err := initAction(ctx) + if err != nil { + log.Fatalf("Error initializing action: %v", err) + } + + // Run the frizbee action + err = frizbeeAction.Run(ctx) + if err != nil { + if errors.Is(err, action.ErrUnpinnedFound) { + log.Printf("Unpinned actions or container images found. Check the Frizbee Action logs for more information.") + os.Exit(1) + } + log.Fatalf("Error running action: %v", err) + } +} + +// initAction initializes the frizbee action - reads the environment variables, creates the GitHub client, etc. +func initAction(ctx context.Context) (*action.FrizbeeAction, error) { + // Get the GitHub token from the environment + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + return nil, fmt.Errorf("GITHUB_TOKEN environment variable is not set") + } + + // Create a new GitHub client + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(ctx, ts) + + // Get the GITHUB_REPOSITORY_OWNER + repoOwner := os.Getenv("GITHUB_REPOSITORY_OWNER") + if repoOwner == "" { + return nil, fmt.Errorf("GITHUB_REPOSITORY_OWNER environment variable is not set") + } + + // Split the GITHUB_REPOSITORY environment variable to get repo name + repoFullName := os.Getenv("GITHUB_REPOSITORY") + if repoFullName == "" { + return nil, fmt.Errorf("GITHUB_REPOSITORY environment variable is not set") + } + + // Clone the repository + fs, repo, err := cloneRepository(fmt.Sprintf("https://github.com/%s", repoFullName), repoOwner, token) + if err != nil { + return nil, fmt.Errorf("failed to clone repository: %w", err) + } + + // Read the action settings from the environment and create the new frizbee replacers for actions and images + return &action.FrizbeeAction{ + Client: github.NewClient(tc), + Token: token, + RepoOwner: repoOwner, + RepoName: strings.TrimPrefix(repoFullName, repoOwner+"/"), + ActionsPath: os.Getenv("INPUT_ACTIONS"), + DockerfilesPath: os.Getenv("INPUT_DOCKERFILES"), + KubernetesPath: os.Getenv("INPUT_KUBERNETES"), + DockerComposePath: os.Getenv("INPUT_DOCKER_COMPOSE"), + OpenPR: os.Getenv("INPUT_OPEN_PR") == "true", + FailOnUnpinned: os.Getenv("INPUT_FAIL_ON_UNPINNED") == "true", + ActionsReplacer: replacer.NewGitHubActionsReplacer(&config.Config{}).WithGitHubClientFromToken(token), + ImagesReplacer: replacer.NewContainerImagesReplacer(&config.Config{}), + BFS: fs, + Repo: repo, + }, nil +} + +// cloneRepository clones the repository and returns a billy.Filesystem interface to interact with it +func cloneRepository(url, owner, accessToken string) (billy.Filesystem, *git.Repository, error) { + fs := memfs.New() + // Use memory storage to clone the repository in memory + store := memory.NewStorage() + repo, err := git.Clone(store, fs, &git.CloneOptions{ + URL: url, + Auth: &http.BasicAuth{ + Username: owner, + Password: accessToken, + }, + }) + if err != nil { + return nil, nil, err + } + return fs, repo, nil } diff --git a/pkg/action/action.go b/pkg/action/action.go new file mode 100644 index 0000000..468577b --- /dev/null +++ b/pkg/action/action.go @@ -0,0 +1,251 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + "fmt" + mapset "github.com/deckarep/golang-set/v2" + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/google/go-github/v60/github" + "github.com/stacklok/frizbee/pkg/replacer" + "log" + "os" + "time" +) + +type FrizbeeAction struct { + Client *github.Client + Token string + RepoOwner string + RepoName string + ActionsPath string + DockerfilesPath string + KubernetesPath string + DockerComposePath string + OpenPR bool + FailOnUnpinned bool + ActionsReplacer *replacer.Replacer + ImagesReplacer *replacer.Replacer + BFS billy.Filesystem + Repo *git.Repository +} + +// Run runs the frizbee action +func (fa *FrizbeeAction) Run(ctx context.Context) error { + // result holds all the processed and modified files + out := &replacer.ReplaceResult{Processed: make([]string, 0), Modified: make(map[string]string)} + + // Parse the workflow files + if err := fa.parseWorkflowActions(ctx, out); err != nil { + return fmt.Errorf("failed to parse workflow files: %w", err) + } + + // Parse all yaml/yml files referencing container images + if err := fa.parseImages(ctx, out); err != nil { + return fmt.Errorf("failed to parse image files: %w", err) + } + log.Printf("Processing output...") + // Process the output + return fa.processOutput(ctx, out) +} + +// parseWorkflowActions parses the GitHub Actions workflow files +func (fa *FrizbeeAction) parseWorkflowActions(ctx context.Context, out *replacer.ReplaceResult) error { + if fa.ActionsPath == "" { + log.Printf("Workflow path is empty") + return nil + } + + log.Printf("Parsing workflow files in %s...", fa.ActionsPath) + res, err := fa.ActionsReplacer.ParsePathInFS(ctx, fa.BFS, fa.ActionsPath) + if err != nil { + return fmt.Errorf("failed to parse workflow files in %s: %w", fa.ActionsPath, err) + } + + // Copy the processed and modified files to the output + out.Processed = mapset.NewSet(out.Processed...).Union(mapset.NewSet(res.Processed...)).ToSlice() + for key, value := range res.Modified { + out.Modified[key] = value + } + return nil +} + +// parseImages parses the Dockerfiles, Docker Compose, and Kubernetes files for container images. +func (fa *FrizbeeAction) parseImages(ctx context.Context, out *replacer.ReplaceResult) error { + pathsToParse := []string{fa.DockerfilesPath, fa.DockerComposePath, fa.KubernetesPath} + for _, path := range pathsToParse { + if path == "" { + continue + } + log.Printf("Parsing files for container images in %s", path) + res, err := fa.ImagesReplacer.ParsePathInFS(ctx, fa.BFS, path) + if err != nil { + return fmt.Errorf("failed to parse: %w", err) + } + // Copy the processed and modified files to the output + out.Processed = mapset.NewSet(out.Processed...).Union(mapset.NewSet(res.Processed...)).ToSlice() + for key, value := range res.Modified { + out.Modified[key] = value + } + } + return nil +} + +// processOutput processes the output of a replacer, prints the processed and modified files and writes the +// changes to the files +func (fa *FrizbeeAction) processOutput(ctx context.Context, res *replacer.ReplaceResult) error { + // Show the processed files + if len(res.Processed) != 0 { + log.Printf("Processed the following files:") + for _, path := range res.Processed { + log.Printf("* %s", path) + } + } else { + log.Printf("No files were processed") + return nil + } + + if len(res.Modified) != 0 { + log.Printf("Modified the following files:") + // Process the modified files + for path, content := range res.Modified { + log.Printf("* %s", path) + log.Printf("%s\n", content) + // Overwrite the content of the file with the changes if the OpenPR flag is set + if fa.OpenPR { + if err := fa.commitChanges(path, content); err != nil { + return fmt.Errorf("failed to commit changes: %w", err) + } + } + } + if fa.OpenPR { + // Create a new pull request + if err := fa.createPR(ctx); err != nil { + return fmt.Errorf("failed to create PR: %w", err) + } + } + // Fail if the FailOnUnpinned flag is set and any files were modified + if fa.FailOnUnpinned { + return ErrUnpinnedFound + } + } + return nil +} + +func (fa *FrizbeeAction) commitChanges(path, content string) error { + f, err := fa.BFS.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", path, err) + } + defer func() { + if err := f.Close(); err != nil { + log.Fatalf("failed to close file %s: %v", path, err) // nolint:errcheck + } + }() + _, err = fmt.Fprintf(f, "%s", content) + if err != nil { + return fmt.Errorf("failed to write to file %s: %w", path, err) + } + + // Stage the file + worktree, err := fa.Repo.Worktree() + if err != nil { + log.Fatalf("failed to get worktree: %v", err) + } + _, err = worktree.Add(path) + if err != nil { + log.Fatalf("failed to add file to staging area: %v", err) + } + + // Commit the change + _, err = worktree.Commit(fmt.Sprintf("Update %s by pinning its image references", path), &git.CommitOptions{ + Author: &object.Signature{ + Name: "github-actions[bot]", + Email: "github-actions[bot]@users.noreply.github.com", + When: time.Now(), + }, + }) + if err != nil { + log.Fatalf("failed to commit changes: %v", err) + } + return nil +} + +// createPR creates a new pull request with the changes made so far +func (fa *FrizbeeAction) createPR(ctx context.Context) error { + // Create a new branch for the PR + branchName := "frizbee-action-patch" + + // Check if a PR already exists for the branch and return if it does + openPrs, _, err := fa.Client.PullRequests.List(ctx, fa.RepoOwner, fa.RepoName, &github.PullRequestListOptions{ + State: "open", + }) + if err != nil { + return err + } + for _, pr := range openPrs { + if pr.GetHead().GetRef() == branchName { + fmt.Printf("PR %d already exists\n", pr.GetNumber()) + return nil + } + } + + headRef, err := fa.Repo.Head() + if err != nil { + log.Fatalf("failed to get head reference: %v", err) + } + branchRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName(branchName), headRef.Hash()) + err = fa.Repo.Storer.SetReference(branchRef) + if err != nil { + log.Fatalf("failed to create branch: %v", err) + } + + // Push the new branch + err = fa.Repo.Push(&git.PushOptions{ + Auth: &http.BasicAuth{ + Username: fa.RepoOwner, + Password: fa.Token, + }, + RefSpecs: []config.RefSpec{ + config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", branchName, branchName)), + }, + }) + if err != nil { + log.Fatalf("failed to push branch: %v", err) + } + + fmt.Printf("Branch %s pushed successfully\n", branchName) + + // Create a new PR + pr, _, err := fa.Client.PullRequests.Create(ctx, fa.RepoOwner, fa.RepoName, &github.NewPullRequest{ + Title: github.String("Frizbee: Pin images and actions to commit hash"), + Body: github.String("The following PR pins images and actions to their commit hash"), + Head: github.String(branchName), + Base: github.String("main"), + MaintainerCanModify: github.Bool(true), + }) + if err != nil { + return err + } + fmt.Printf("PR %d created successfully\n", pr.GetNumber()) + return nil +} diff --git a/pkg/action/errors.go b/pkg/action/errors.go new file mode 100644 index 0000000..4b910e0 --- /dev/null +++ b/pkg/action/errors.go @@ -0,0 +1,21 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import "errors" + +// ErrUnpinnedFound is the error returned when unpinned actions or container images are found +var ErrUnpinnedFound = errors.New("frizbee found unpinned actions or container images") diff --git a/pkg/githubapi/githubapi.go b/pkg/githubapi/githubapi.go deleted file mode 100644 index 54ac6ad..0000000 --- a/pkg/githubapi/githubapi.go +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright 2024 Stacklok, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package githubapi - -import ( - "context" - - "github.com/google/go-github/v60/github" - "golang.org/x/oauth2" -) - -type GitHubClient struct { - Client *github.Client - Ctx context.Context -} - -// NewGitHubClient creates a new instance of GitHubClient with the provided token. -// It initializes the GitHub API client using the token for authentication. -// The returned GitHubClient can be used to interact with the GitHub API. -func NewGitHubClient(token string) *GitHubClient { - ctx := context.Background() - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) - tc := oauth2.NewClient(ctx, ts) - client := github.NewClient(tc) - - return &GitHubClient{ - Client: client, - Ctx: ctx, - } -} diff --git a/tests/docker_compose/docker-compose.yaml b/tests/docker_compose/docker-compose.yaml new file mode 100644 index 0000000..e18049f --- /dev/null +++ b/tests/docker_compose/docker-compose.yaml @@ -0,0 +1,193 @@ +# +# Copyright 2023 Stacklok, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +version: '3.2' +services: + minder: + container_name: minder_server + build: + context: . + dockerfile: ./docker/minder/Dockerfile + image: minder:latest + + command: [ + "serve", + "--grpc-host=0.0.0.0", + "--http-host=0.0.0.0", + "--metric-host=0.0.0.0", + "--db-host=postgres", + "--issuer-url=http://keycloak:8080", + "--config=/app/server-config.yaml", + # If you don't want to store your GitHub client ID and secret in the main + # config file, point to them here: + # "--github-client-id-file=/secrets/github_client_id", + # "--github-client-secret-file=/secrets/github_client_secret", + ] + restart: always # keep the server running + read_only: true + ports: + - "8080:8080" + - "8090:8090" + - "9090:9090" + volumes: + - ./server-config.yaml:/app/server-config.yaml:z + - ./flags-config.yaml:/app/flags-config.yaml:z + # If you don't want to store your GitHub client ID and secret in the main + # config file, point to them here: + # - ./.github_client_id:/secrets/github_client_id:z + # - ./.github_client_secret:/secrets/github_client_secret:z + # If you're using a GitHub App, you'll need to provide the private key: + - ./.secrets/:/app/.secrets/:z + - ./.ssh:/app/.ssh:z + environment: + - KO_DATA_PATH=/app/ + # Use viper environment variables to set specific paths to keys; + # these values are relative paths in server-config.yaml, but it's not clear + # what they are relative _to_... + - MINDER_AUTH_ACCESS_TOKEN_PRIVATE_KEY=/app/.ssh/access_token_rsa + - MINDER_AUTH_ACCESS_TOKEN_PUBLIC_KEY=/app/.ssh/access_token_rsa.pub + - MINDER_AUTH_REFRESH_TOKEN_PRIVATE_KEY=/app/.ssh/refresh_token_rsa + - MINDER_AUTH_REFRESH_TOKEN_PUBLIC_KEY=/app/.ssh/refresh_token_rsa.pub + - MINDER_AUTH_TOKEN_KEY=/app/.ssh/token_key_passphrase + - MINDER_UNSTABLE_TRUSTY_ENDPOINT=https://api.trustypkg.dev + - MINDER_PROVIDER_GITHUB_APP_PRIVATE_KEY=/app/.secrets/github-app.pem + - MINDER_FLAGS_GO_FEATURE_FILE_PATH=/app/flags-config.yaml + - MINDER_LOG_GITHUB_REQUESTS=1 + networks: + - app_net + depends_on: + postgres: + condition: service_healthy + keycloak: + condition: service_healthy + openfga: + condition: service_healthy + migrate: + condition: service_completed_successfully + keycloak-config: + condition: service_completed_successfully + migrate: + container_name: minder_migrate_up + build: + context: . + dockerfile: ./docker/minder/Dockerfile + image: minder:latest + + command: [ + "migrate", + "up", + "--yes", + "--db-host=postgres", + "--config=/app/server-config.yaml", + ] + volumes: + - ./server-config.yaml:/app/server-config.yaml:z + - ./database/migrations:/app/database/migrations:z + environment: + - KO_DATA_PATH=/app/ + networks: + - app_net + deploy: + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + depends_on: + postgres: + condition: service_healthy + openfga: + condition: service_healthy + postgres: + container_name: postgres_container + image: postgres:16.2-alpine + restart: always + user: postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: minder + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - app_net + + keycloak: + container_name: keycloak_container + image: quay.io/keycloak/keycloak:23.0 + command: ["start-dev"] + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_HEALTH_ENABLED: "true" + healthcheck: + test: ["CMD", "/opt/keycloak/bin/kcadm.sh", "config", "credentials", "--server", "http://localhost:8080", "--realm", "master", "--user", "admin", "--password", "admin"] + interval: 10s + timeout: 5s + retries: 10 + ports: + - "8081:8080" + volumes: + - ./identity/themes:/opt/keycloak/themes:z + networks: + - app_net + + keycloak-config: + container_name: keycloak_config + image: bitnami/keycloak-config-cli:5.10.0 + entrypoint: ["java", "-jar", "/opt/bitnami/keycloak-config-cli/keycloak-config-cli.jar"] + environment: + KEYCLOAK_URL: http://keycloak:8080 + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: admin + KC_MINDER_SERVER_SECRET: secret + IMPORT_VARSUBSTITUTION_ENABLED: "true" + IMPORT_FILES_LOCATIONS: /config/*.yaml + volumes: + - ./identity/config:/config:z + networks: + - app_net + + depends_on: + keycloak: + condition: service_healthy + + openfga: + container_name: openfga + image: openfga/openfga:v1.5.0 + command: [ + "run", + "--playground-port=8085" + ] + healthcheck: + test: + - CMD + - grpc_health_probe + - "-addr=:8081" + ports: + - 8082:8080 + - 8083:8081 + - 8085:8085 + networks: + - app_net +networks: + app_net: + driver: bridge + diff --git a/tests/dockerfiles/Dockerfile b/tests/dockerfiles/Dockerfile new file mode 100644 index 0000000..842692f --- /dev/null +++ b/tests/dockerfiles/Dockerfile @@ -0,0 +1,8 @@ +FROM golang:alpine3.19 +RUN apk add git + +COPY . /home/src +WORKDIR /home/src +RUN go build -o /bin/action ./ + +ENTRYPOINT [ "/bin/action" ] diff --git a/tests/dockerfiles/Dockerfile.prod b/tests/dockerfiles/Dockerfile.prod new file mode 100644 index 0000000..a19b7fa --- /dev/null +++ b/tests/dockerfiles/Dockerfile.prod @@ -0,0 +1,8 @@ +FROM golang +RUN apk add git + +COPY . /home/src +WORKDIR /home/src +RUN go build -o /bin/action ./ + +ENTRYPOINT [ "/bin/action" ] diff --git a/tests/k8s/pod.yml b/tests/k8s/pod.yml new file mode 100644 index 0000000..325ae61 --- /dev/null +++ b/tests/k8s/pod.yml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Pod +metadata: + name: mount-host + namespace: playground +spec: + containers: + - name: mount-host + image: alpine + command: ["sleep"] + args: ["infinity"] + volumeMounts: + - name: host-root + mountPath: /host + readOnly: true + volumes: + - name: host-root + hostPath: + path: / + type: Directory diff --git a/tests/workflows/build.yml b/tests/workflows/build.yml new file mode 100644 index 0000000..267e479 --- /dev/null +++ b/tests/workflows/build.yml @@ -0,0 +1,15 @@ +on: + workflow_call: +jobs: + build: + name: Verify build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.6 + - name: Extract version of Go to use + run: echo "GOVERSION=$(sed -n 's/^go \([0-9.]*\)/\1/p' go.mod)" >> $GITHUB_ENV + - uses: actions/setup-go@v5.0.1 + with: + go-version-file: 'go.mod' + - name: build + run: make build diff --git a/tests/workflows/test.yml b/tests/workflows/test.yml new file mode 100644 index 0000000..f5101eb --- /dev/null +++ b/tests/workflows/test.yml @@ -0,0 +1,15 @@ +on: + push: + workflow_dispatch: +permissions: write-all +jobs: + test: + runs-on: ubuntu-latest + steps: + # To use this repository's private action, + # you must check out the repository + - name: Checkout + uses: actions/checkout@v4 + - uses: ./ # Uses an action in the root directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}