From 8a354ad4687be55dbf8b3d009e3c9ce7d1daefac Mon Sep 17 00:00:00 2001 From: Derek Roberts Date: Tue, 5 Dec 2023 17:40:34 -0800 Subject: [PATCH] feat: setup action --- .github/codeowners | 7 ++ .github/workflows/pr-open.yml | 60 +++++++++ .gitignore | 36 +----- README.md | 2 - action.yml | 191 +++++++++++++++++++++++++++++ LICENSE => license | 0 readme.md | 224 ++++++++++++++++++++++++++++++++++ renovate.json | 7 ++ 8 files changed, 494 insertions(+), 33 deletions(-) create mode 100644 .github/codeowners create mode 100644 .github/workflows/pr-open.yml delete mode 100644 README.md create mode 100644 action.yml rename LICENSE => license (100%) create mode 100644 readme.md create mode 100644 renovate.json diff --git a/.github/codeowners b/.github/codeowners new file mode 100644 index 0000000..5f9c14d --- /dev/null +++ b/.github/codeowners @@ -0,0 +1,7 @@ +# Matched against repo root (asterisk) +* @mishraomp @paulushcgcj @DerekRoberts + +# Matched against directories +# /.github/workflows/ @mishraomp @paulushcgcj @DerekRoberts + +# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml new file mode 100644 index 0000000..172d21b --- /dev/null +++ b/.github/workflows/pr-open.yml @@ -0,0 +1,60 @@ +name: PR + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # QuickStart apps build from the same dirs as their Dockerfiles + build: + permissions: + packages: write + name: Build + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: ./ + with: + package: backend + keep_versions: 10 + repository: bcgov/quickstart-openshift + tag: ${{ github.event.number }} + + # Anything can be retagged if a fallback image exists (e.g. test) + retag: + needs: [build] + permissions: + packages: write + name: Retag + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: ./ + with: + package: backend + keep_versions: 10 + repository: bcgov/quickstart-openshift + tag: ${{ github.event.number }}-retag + tag_fallback: test + triggers: ('backend/') + + + # FOM apps build from repo root, above their Dockerfiles (extra params) + advanced: + permissions: + packages: write + name: Advanced + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: ./ + with: + package: api + build_context: . + build_file: api/Dockerfile + keep_versions: 10 + repository: bcgov/nr-fom + tag: ${{ github.event.number }} diff --git a/.gitignore b/.gitignore index c6bba59..6704566 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* -.pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json @@ -42,8 +41,8 @@ build/Release node_modules/ jspm_packages/ -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ +# TypeScript v1 declaration files +typings/ # TypeScript cache *.tsbuildinfo @@ -54,9 +53,6 @@ web_modules/ # Optional eslint cache .eslintcache -# Optional stylelint cache -.stylelintcache - # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ @@ -72,20 +68,15 @@ web_modules/ # Yarn Integrity file .yarn-integrity -# dotenv environment variable files +# dotenv environment variables file .env -.env.development.local -.env.test.local -.env.production.local -.env.local +.env.test # parcel-bundler cache (https://parceljs.org/) .cache -.parcel-cache # Next.js build output .next -out # Nuxt.js build / generate output .nuxt @@ -93,20 +84,13 @@ dist # Gatsby files .cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js +# Comment in the public line in if your project uses Gatsby and *not* Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - # Serverless directories .serverless/ @@ -118,13 +102,3 @@ dist # TernJS port file .tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* diff --git a/README.md b/README.md deleted file mode 100644 index e99f9ee..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# action-get-pr -Get PR number for merge and queue events diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..9f48824 --- /dev/null +++ b/action.yml @@ -0,0 +1,191 @@ +name: Conditional Container Builder with Fallback +description: Build if trigger conditions are met, else use fallback image +branding: + icon: package + color: blue + +inputs: + ### Required + package: + description: Package name; e.g. backend, frontend + required: true + tag: + description: Default tag; e.g. pr#, test, prod + required: true + + ### Typical / recommended + tag_fallback: + description: Where to pull default images from; e.g. prod, test + triggers: + description: Paths used to trigger a build; e.g. ('./backend/' './frontend/) + build_context: + description: Build context, not required for self-contained package/default directory + build_file: + description: Dockerfile with path, not required for self-contained package/default directory + keep_versions: + description: Number of versions to keep; omit to skip + + ### Usually a bad idea / not recommended + build_args: + description: A list of build-time variables, generally not adviseable + value: "BUILDKIT_INLINE_CACHE=1" + diff_branch: + description: Branch to diff against + default: ${{ github.event.repository.default_branch }} + keep_regex: + description: Regex for tags to skip when keep_versions is provided; defaults to test and prod + default: "^(prod|test|latest)$" + repository: + description: Non-default repo to clone + default: ${{ github.repository }} + token: + description: Specify token (GH or PAT), instead of inheriting one from the calling workflow + default: ${{ github.token }} + +outputs: + digest: + description: 'Digest of the built image. for ex: sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + value: ${{ steps.get_digest.outputs.digest }} + +runs: + using: composite + steps: + - uses: actions/checkout@v4 + with: + # Check out build repo + repository: ${{ inputs.repository }} + + # Process variables and inputs + - id: vars + shell: bash + run: | + # Inputs and variables + + # Use package folder as build_context unless an override has been provided + if [ -z ${{ inputs.build_context }} ]; then + BUILD_CONTEXT=${{ inputs.package }} + else + BUILD_CONTEXT=${{ inputs.build_context }} + fi + echo "build_context=${BUILD_CONTEXT}" >> $GITHUB_OUTPUT + + # Use BUILD_CONTEXT/Dockerfile as build_file unless an override has been provided + if [ -z ${{ inputs.build_file }} ]; then + BUILD_FILE=${BUILD_CONTEXT}/Dockerfile + else + BUILD_FILE=${{ inputs.build_file }} + fi + echo "build_file=${BUILD_FILE}" >> $GITHUB_OUTPUT + + # Bug - Docker build hates images with capital letters + TAGS=$( echo "ghcr.io/${{ github.repository }}/${{ inputs.package }}:${{ inputs.tag }}" | tr '[:upper:]' '[:lower:]' ) + echo "tags=${TAGS//[$'\r\n ']}" >> $GITHUB_OUTPUT + + # Check if a build is required (steps.build.outputs.triggered=true|false) + - name: Check for builds + shell: bash + id: build + run: | + # Check for builds + + # Build if an override repository was provided + if [ "${{ inputs.repository }}" != "${{ github.repository }}" ]; then + echo "Build triggered on override repository" + echo "triggered=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Build if no tag_fallback or no triggers + if [ -z "${{ inputs.tag_fallback }}" ]||[ -z "${{ inputs.triggers }}" ]; then + echo "Build triggered with possible reasons:" + echo " a) tag_fallback provided" + echo " b) triggers not provided" + echo "triggered=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Build if tag_fallback is no good (requires a valid container) + FALLBACK=ghcr.io/${{ inputs.repository }}/${{ inputs.package }}:${{ inputs.tag_fallback }} + if [ "$(docker buildx imagetools inspect ${FALLBACK} || true | grep -qi '^Name:')" = "" ]; then + # Output triggered=true for next steps + echo "Build triggered. Fallback tag (tag_fallback) not usable." + echo "Manifest checked for: ghcr.io/${FALLBACK}" + echo "triggered=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Build if changed files (git diff) match triggers + TRIGGERS=${{ inputs.triggers }} + git fetch origin ${{ inputs.diff_branch }} + while read -r check; do + for t in "${TRIGGERS[@]}"; do + if [[ "${check}" =~ "${t}" ]]; then + # Output triggered=true for next steps + echo "Build triggered based on git diff" + echo -e "${t}\n --> ${check}" + echo "triggered=true" >> $GITHUB_OUTPUT + exit 0 + fi + done + done < <(git diff origin/${{ inputs.diff_branch }} --name-only) + + # If at this point, no build is required + echo "Container build not required" + echo "triggered=false" >> $GITHUB_OUTPUT + + # If a build is not required, reuse a previous image + - name: Recycle/retag Previous Images + uses: shrink/actions-docker-registry-tag@v3 + if: steps.build.outputs.triggered != 'true' + with: + registry: ghcr.io + repository: ${{ inputs.repository }}/${{ inputs.package }} + target: ${{ inputs.tag_fallback }} + tags: ${{ inputs.tag }} + + # If a build is required, then login, build and push! + - name: Set up Docker Buildx + if: steps.build.outputs.triggered == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + if: steps.build.outputs.triggered == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.token }} + + - name: Build and push ${{ inputs.package }} Docker image + if: steps.build.outputs.triggered == 'true' + uses: docker/build-push-action@v5 + with: + context: ${{ steps.vars.outputs.build_context }} + file: ${{ steps.vars.outputs.build_file }} + push: true + tags: ${{ steps.vars.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: ${{ inputs.build_args }} + + # Cleanup if inputs.keep_versions provided + - name: GHCR Cleanup + if: ${{ inputs.keep_versions }} + uses: actions/delete-package-versions@v4 + with: + package-name: "${{ github.event.repository.name }}/${{ inputs.package }}" + package-type: "container" + min-versions-to-keep: ${{ inputs.keep_versions }} + ignore-versions: "${{ inputs.keep_regex }}" + + # Action repo needs to be present for cleanup/tests + - name: Checkout local repo to make sure action.yml is present + if: ${{ github.repository }} != ${{ inputs.repository }} + uses: actions/checkout@v4 + + - name: Return digest of the built image + id: get_digest + shell: bash + run: | + DIGEST=$(docker manifest inspect ${{ steps.vars.outputs.tags }} | jq '.manifests[0].digest') + echo "digest=${DIGEST}" >> $GITHUB_OUTPUT diff --git a/LICENSE b/license similarity index 100% rename from LICENSE rename to license diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..84b703b --- /dev/null +++ b/readme.md @@ -0,0 +1,224 @@ + +[![Issues](https://img.shields.io/github/issues/bcgov-nr/action-conditional-container-builder)](/../../issues) +[![Pull Requests](https://img.shields.io/github/issues-pr/bcgov-nr/action-conditional-container-builder)](/../../pulls) +[![MIT License](https://img.shields.io/github/license/bcgov-nr/action-conditional-container-builder.svg)](/LICENSE) +[![Lifecycle](https://img.shields.io/badge/Lifecycle-Experimental-339999)](https://github.com/bcgov/repomountie/blob/master/doc/lifecycle-badges.md) + +# Conditional Container Builder with Fallback + +This action builds Docker/Podman containers conditionally using a set of directories. If any files were changed matching that, then build a container. If those files were not changed, retag an existing build. + +This is useful in CI/CD pipelines where not every package/app needs to be rebuilt. + +This tool is currently strongly opinionated and generatess images with a rigid structure below. This is intended to become more flexible in future. + +Package name: `//:` + +Pull with: `docker pull ghcr.io///:` + +Only GitHub Container Registry (ghcr.io) is supported so far. + +# Usage + +```yaml +- uses: bcgov-nr/action-builder-ghcr@vX.Y.X + with: + ### Required + + # Package name + package: frontend + + # Tag name (:) + tag: ${{ github.event.number }} + + + ### Typical / recommended + + # Fallback tag, used if no build was generated + # Optional, defaults to nothing, which forces a build + # Non-matching or malformed tags are rejected, which also forced a build + tag_fallback: test + + # Bash array to diff for build triggering + # Optional, defaults to nothing, which forces a build + triggers: ('frontend/' 'backend/' 'database/') + + # Sets the build context/directory, which contains the build files + # Optional, defaults to package name + build_context: ./frontend + + # Sets the Dockerfile with path + # Optional, defaults to {package}/Dockerfile or {build_context}/Dockerfile + build_file: ./frontend/Dockerfile + + # Number of packages to keep if cleaning up previous builds + # Optional, skips if not provided + keep_versions: 50 + + + ### Usually a bad idea / not recommended + + # Sets a list of [build-time variables](https://docs.docker.com/engine/reference/commandline/buildx_build/#build-arg) + # Optional, defaults to sample content + build_args: | + ENV=build + + # Overrides the default branch to diff against + # Defaults to the default branch, usually `main` + diff_branch: ${{ github.event.repository.default_branch }} + + # Regex for tags to skip when cleaning up packages; defaults to test and prod + # Only used when keep_versions is provided + keep_regex: "^(prod|test)$" + + # Repository to clone and process + # Useful for consuming other repos, like in testing + # Defaults to the current one + repository: ${{ github.repository }} + + # Specify token (GH or PAT), instead of inheriting one from the calling workflow + token: ${{ secrets.GITHUB_TOKEN }} + +``` + +# Example, Single Build + +Build a single subfolder with a Dockerfile in it. Deletes old packages, keeping the last 50. Runs on pull requests (PRs). + +Create or modify a GitHub workflow, like below. E.g. `./github/workflows/pr-open.yml` + +```yaml +name: Pull Request + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + builds: + permissions: + packages: write + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Builds + uses: bcgov-nr/action-builder-ghcr@vX.Y.Z + with: + package: frontend + keep_versions: 50 + tag: ${{ github.event.number }} + tag_fallback: test + token: ${{ secrets.GITHUB_TOKEN }} + triggers: ('frontend/') +``` + +# Example, Single Build with build_context and build_file + +Same as previous, but specifying build folder and Dockerfile. + +Create or modify a GitHub workflow, like below. E.g. `./github/workflows/pr-open.yml` + +```yaml +name: Pull Request + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + builds: + permissions: + packages: write + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Builds + uses: bcgov-nr/action-builder-ghcr@vX.Y.Z + with: + package: frontend + build_context: ./ + build_file: subdir/Dockerfile + keep_versions: 50 + tag: ${{ github.event.number }} + tag_fallback: test + token: ${{ secrets.GITHUB_TOKEN }} + triggers: ('frontend/') +``` + +# Example, Matrix Build + +Build from multiple subfolders with Dockerfile in them. This time an outside repository is used. Runs on pull requests (PRs). + +Create or modify a GitHub workflow, like below. E.g. `./github/workflows/pr-open.yml` + +```yaml +name: Pull Request + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + builds: + permissions: + packages: write + runs-on: ubuntu-22.04 + strategy: + matrix: + package: [backend, frontend] + include: + - package: backend + triggers: ('backend/') + - package: frontend + triggers: ('frontend/') + steps: + - uses: actions/checkout@v3 + - name: Test Builds + uses: bcgov-nr/action-builder-ghcr@vX.Y.Z + with: + package: ${{ matrix.package }} + tag: ${{ github.event.number }} + tag_fallback: test + repository: bcgov/nr-quickstart-typescript + token: ${{ secrets.GITHUB_TOKEN }} + triggers: ${{ matrix.triggers }} + +``` + +# Output + +The build will return image digests as output. + +```yaml +- id: meaningful_id_name + uses: bcgov-nr/action-builder-ghcr@vX.Y.Z + ... + +- id: deploy_with_digest + name: Deploy with digest + with: + digest: ${{ steps.meaningful_id_name.outputs.digest }} + ... +``` + +# Permissions + +Workflows kicked off by Dependabot or a fork run with reduced permissions. That can be addressed by setting explict permissions for the GITHUB_TOKEN. If this is not required, then remove the lines below from these examples. + +```yaml +permissions: + packages: write +``` + + diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..fbf7e58 --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "description": "Presets from https://github.com/bcgov/nr-renovate", + "extends": [ + "github>bcgov/renovate-config" + ] +}