From df0e29878626f11404d3041c2427b875d03377c7 Mon Sep 17 00:00:00 2001 From: Dagan McGregor Date: Mon, 30 Mar 2026 19:34:09 +1300 Subject: [PATCH] fix: update README.md file to list explicit secure practices to apply Update README.md to explicitly list secure practices for updating GItHub Actions taken from GitHub security documentation and examples from a few security blogs. When GeoNet migrated to GitHub Actions it was noted at the time to use the full-length commit SHA value to securely use external 3rd-party code to avoid the sort of supply chain attaks seen with Trivy scanner and malicious overwriting of all version tags for Trivy scanner GitHub actions. --- .github/workflows/presubmit-workflow-lint.yml | 4 +- .../reusable-container-image-scan.yml | 34 +- README.md | 1314 +---------------- USAGE.md | 1308 ++++++++++++++++ full-length-commit-sha.png | Bin 0 -> 7745 bytes mise.toml | 2 + 6 files changed, 1378 insertions(+), 1284 deletions(-) create mode 100644 USAGE.md create mode 100644 full-length-commit-sha.png create mode 100644 mise.toml diff --git a/.github/workflows/presubmit-workflow-lint.yml b/.github/workflows/presubmit-workflow-lint.yml index 9007ec67..36d1d8b4 100644 --- a/.github/workflows/presubmit-workflow-lint.yml +++ b/.github/workflows/presubmit-workflow-lint.yml @@ -37,8 +37,8 @@ jobs: run: | FAILURES=false for WORKFLOW in $(find .github/workflows -name 'reusable*' | xargs); do - if ! grep -q "$WORKFLOW" README.md; then - echo "Not found in README: $WORKFLOW" + if ! grep -q "$WORKFLOW" USAGE.md; then + echo "Not found in USAGE.md: $WORKFLOW" FAILURES=true fi done diff --git a/.github/workflows/reusable-container-image-scan.yml b/.github/workflows/reusable-container-image-scan.yml index 6dee2876..f7850084 100644 --- a/.github/workflows/reusable-container-image-scan.yml +++ b/.github/workflows/reusable-container-image-scan.yml @@ -68,20 +68,20 @@ jobs: echo "$DESTINATION_DIGEST" ) | column -t echo "destination=${DESTINATION_DIGEST}" >> $GITHUB_OUTPUT - - name: Run Trivy vulnerability scanner - if: ${{ steps.get-digests.outputs.destination != null }} - uses: aquasecurity/trivy-action@062f2592684a31eb3aa050cc61e7ca1451cecd3d # 0.18.0 - env: - TRIVY_USERNAME: ${{ github.actor }} - TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - with: - scan-type: image - image-ref: "${{ fromJSON(toJSON(matrix)).target }}" - format: "sarif" - output: "trivy-results.sarif" - severity: 'HIGH,CRITICAL' - - name: Upload Trivy scan results to GitHub Security tab - if: ${{ steps.get-digests.outputs.destination != null }} - uses: github/codeql-action/upload-sarif@3ab4101902695724f9365a384f86c1074d94e18c # 3.24.7 - with: - sarif_file: "trivy-results.sarif" + # - name: Run Trivy vulnerability scanner + # if: ${{ steps.get-digests.outputs.destination != null }} + # uses: aquasecurity/trivy-action@062f2592684a31eb3aa050cc61e7ca1451cecd3d # 0.18.0 + # env: + # TRIVY_USERNAME: ${{ github.actor }} + # TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + # with: + # scan-type: image + # image-ref: "${{ fromJSON(toJSON(matrix)).target }}" + # format: "sarif" + # output: "trivy-results.sarif" + # severity: 'HIGH,CRITICAL' + # - name: Upload Trivy scan results to GitHub Security tab + # if: ${{ steps.get-digests.outputs.destination != null }} + # uses: github/codeql-action/upload-sarif@3ab4101902695724f9365a384f86c1074d94e18c # 3.24.7 + # with: + # sarif_file: "trivy-results.sarif" diff --git a/README.md b/README.md index 5c12facb..a9a4f83d 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,12 @@ - + - [Actions](#actions) - [Workflows](#workflows) - - [Ko build](#ko-build) - - [Docker build](#docker-build) - - [Dockerfile lint](#dockerfile-lint) - - [Container image scan](#container-image-scan) - - [Terraform management](#terraform-management) - - [Presubmit Actions workflow require commit digest vet](#presubmit-actions-workflow-require-commit-digest-vet) - - [Presubmit Go code lint](#presubmit-go-code-lint) - - [Go vet](#go-vet) - - [Go fmt](#go-fmt) - - [Go test](#go-test) - - [Go vulnerability check](#go-vulnerability-check) - - [Go build smoke test](#go-build-smoke-test) - - [goimports](#goimports) - - [Presubmit commit policy conformance](#presubmit-commit-policy-conformance) - - [Go container apps](#go-container-apps) - - [Go apps](#go-apps) - - [Bash shellcheck](#bash-shellcheck) - - [Presubmit README table of contents](#presubmit-readme-table-of-contents) - - [Presubmit GitHub Actions workflow validator](#presubmit-github-actions-workflow-validator) - - [GitHub Actions action validator](#github-actions-action-validator) - - [Markdown lint](#markdown-lint) - - [Copy to S3](#copy-to-s3) - - [Clean container versions](#clean-container-versions) - - [ESLint](#eslint) - - [AWS deploy](#aws-deploy) - - [Composite Actions](#composite-actions) - - [Tagging](#tagging) - - [Validate bucket URI](#validate-bucket-uri) - - [Copy to S3](#copy-to-s3-1) - - [Copy from S3](#copy-from-s3) - - [Other documentation](#other-documentation) - - [Dependabot and Actions workflow imports](#dependabot-and-actions-workflow-imports) - - [Versioning for container images](#versioning-for-container-images) - - [Go Versioning in workflows](#go-versioning-in-workflows) + - [Usage examples](#usage-examples) + - [GitHub Actions security and policies](#github-actions-security-and-policies) + - [Dependabot and Actions workflow imports](#dependabot-and-actions-workflow-imports) + - [Versioning for container images](#versioning-for-container-images) + - [Go Versioning in workflows](#go-versioning-in-workflows) # Actions @@ -53,1274 +24,87 @@ There are three types of workflows in this repo - reusable _apps_: combined function workflows which include several other reusable workflows - GeoNet/Actions maintainability: workflows which support the consistency of the workflows in this repo -the workflows are intended to work with and around the maintainers of GeoNet software for automations which are valuable to the project. +The workflows are intended to work with and around the maintainers of GeoNet software for automations which are valuable to the project. - - -### Ko build - -STATUS: stable - -Generic build for containerised Go applications with [Ko](https://ko.build). - -Example: - -```yaml -name: build - -on: - push: {} - release: - types: [published] - workflow_dispatch: {} - -permissions: - packages: write - id-token: write - -jobs: - build: - uses: GeoNet/Actions/.github/workflows/reusable-ko-build.yml@main - # with: - # paths: ./cmd/coolapp - # registryOverride: registry.example.com - # registryGhcrUsernameOverride: ${{ secrets.GHCR_USERNAME }} - # registryGhcrPasswordOverride: ${{ secrets.GHCR_PASSWORD }} - # push: true - # aws-region: ap-southeast-2 - # aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME - # aws-role-duration-seconds: "3600" - # setup: | - # sudo apt install -y something-needed-for-build - # configPath: .ko.yaml -``` - -- dynamic build of images based on entrypoints (where there is a `package main`), unless if _inputs.paths_ is set -- fast! - -Pushing to ECR example: - -```yaml -name: build - -on: - push: {} - release: - types: [published] - workflow_dispatch: {} - -permissions: - packages: write - id-token: write - -jobs: - build: - uses: GeoNet/Actions/.github/workflows/reusable-ko-build.yml@main - with: - registryOverride: $ACCOUNT.dkr.ecr.$REGION.amazonaws.com - aws-region: ap-southeast-2 - aws-role-arn-to-assume: arn:aws:iam::$ACCOUNT:role/$ROLE_NAME - aws-role-duration-seconds: "3600" -``` - -for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-ko-build.yml](.github/workflows/reusable-ko-build.yml). - -### Docker build - -STATUS: stable - -Generic container image build with Docker. - -Single use example: - -```yaml -name: build - -on: - push: {} - release: - types: [published] - workflow_dispatch: {} - -permissions: - packages: write - id-token: write - -jobs: - build: - uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main - with: - context: . - dockerfile: ./Dockerfile - imageName: cool - platforms: 'linux/amd64,linux/arm64' - push: ${{ github.ref == 'refs/heads/main' }} - buildArgs: | - VERSION=${{ github.sha }} -``` - -to add more, copy the block like `jobs.build` and replace values in next block where desired. - -Multiple dynamic parallel builds based on directory subfolders: - -```yaml -name: build - -on: - push: {} - release: - types: [published] - workflow_dispatch: {} - -permissions: - packages: write - id-token: write - -env: - FOLDER: apps - -jobs: - prepare: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set.outputs.matrix }} - steps: - - uses: actions/checkout@v3 - - uses: GeoNet/yq@bbe305500687a5fe8498d74883c17f0f06431ac4 # master - - id: set - run: | - echo "matrix=$(find $FOLDER -mindepth 1 -maxdepth 1 -type d | xargs -n 1 basename | xargs | yq 'split(" ")|.[]|{"target":.}' -ojson | jq -rcM -s .)" >> $GITHUB_OUTPUT - - name: check output - run: | - jq . <<< '${{ steps.set.outputs.matrix }}' - - build: - needs: prepare - strategy: - matrix: - include: ${{ fromJSON(needs.prepare.outputs.matrix) }} - uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main - with: - context: apps/${{ fromJSON(toJSON(matrix)).target }} - dockerfile: apps/${{ fromJSON(toJSON(matrix)).target }}/Dockerfile - imageName: ${{ fromJSON(toJSON(matrix)).target }} - platforms: 'linux/amd64,linux/arm64' -``` - -Pushing to ECR example: - -```yaml -name: build - -on: - push: {} - release: - types: [published] - workflow_dispatch: {} - -permissions: - packages: write - id-token: write - -jobs: - build: - uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main - with: - context: . - dockerfile: ./Dockerfile - imageName: cool - platforms: 'linux/amd64,linux/arm64' - push: ${{ github.ref == 'refs/heads/main' }} - registryOverride: $ACCOUNT.dkr.ecr.$REGION.amazonaws.com - aws-region: ap-southeast-2 - aws-role-arn-to-assume: arn:aws:iam::$ACCOUNT:role/$ROLE_NAME - aws-role-duration-seconds: "3600" -``` - -Pulling in a GitHub artifact for a build: - -```yaml -name: build - -on: - push: {} - release: - types: [published] - workflow_dispatch: {} - -permissions: - packages: write - id-token: write - -jobs: - prepare: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - run: | - mkdir -p ./apps/cool-ng/assets - echo 'hello!' > ./apps/cool-ng/assets/index.html - - name: upload the cool numbers - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: cool-ng - path: ./apps/cool-ng/assets - retention-days: 1 - build: - needs: prepare - uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main - with: - context: ./apps/cool-ng - dockerfile: ./apps/cool-ng/Dockerfile - imageName: cool-ng - platforms: 'linux/amd64,linux/arm64' - push: ${{ github.ref == 'refs/heads/main' }} - artifact-name: cool-ng - artifact-path: ./apps/cool-ng/assets -``` - -Pulling in things from S3 in a build: - -```yaml -name: build - -on: - push: {} - release: - types: [published] - workflow_dispatch: {} - -permissions: - packages: write - id-token: write - -jobs: - copy-from-s3: - uses: GeoNet/Actions/.github/workflows/reusable-copy-to-s3.yml@main - with: - aws-region: ap-southeast-2 - aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME - aws-role-duration-seconds: 3600 - artifact-name: cool-ng - artifact-path: ./apps/cool-ng/assets - s3-bucket: s3://some-really-really-cool-s3-bucket/assets - cp-or-sync: cp - direction: from # 'to' or 'from' - build: - needs: copy-from-s3 - uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main - with: - context: ./apps/cool-ng - dockerfile: ./apps/cool-ng/Dockerfile - imageName: cool-ng - platforms: 'linux/amd64,linux/arm64' - push: ${{ github.ref == 'refs/heads/main' }} - artifact-name: cool-ng - artifact-path: ./apps/cool-ng/assets -``` - -note: $registryOverride + '/' + $imageName must be an existing ECR - -Override auth to ghcr.io - -```yaml -name: build - -on: - push: {} - release: - types: [published] - workflow_dispatch: {} - -permissions: - packages: write - id-token: write - -jobs: - build: - uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main - with: - context: . - dockerfile: ./Dockerfile - imageName: cool - platforms: 'linux/amd64,linux/arm64' - push: ${{ github.ref == 'refs/heads/main' }} - buildArgs: | - VERSION=${{ github.sha }} - registryGhcrUsernameOverride: example - secrets: inherit -``` - -Copy an image to a different container registry: - -```yaml -name: build - -on: - push: {} - release: - types: [published] - workflow_dispatch: {} - -permissions: - packages: write - id-token: write - -env: - VERSION_CRANE: v0.16.1 - -jobs: - build: - uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main - with: - context: . - dockerfile: ./Dockerfile - imageName: cool - platforms: 'linux/amd64,linux/arm64' - push: ${{ github.ref == 'refs/heads/main' }} - copy-image-to-registry: - needs: build - runs-on: ubuntu-latest - steps: - - uses: GeoNet/setup-crane@00c9e93efa4e1138c9a7a5c594acd6c75a2fbf0c # main - with: - version: ${{ env.VERSION_CRANE }} - - name: authenticate to registry - run: | - echo SOME_PASSWORD | crane auth login -u some-user --password-stdin - - name: copy image - env: - SOURCE: ${{ needs.build.outputs.image }} - DESTINATION: ghcr.io/someorg/someimage:sometag - run: | - crane cp "$SOURCE" "$DESTINATION" -``` - -this may be useful for things like image promotion or staging. - -for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-docker-build.yml](.github/workflows/reusable-docker-build.yml). - -### Dockerfile lint - -STATUS: stable - -```yaml -name: dockerfile lint -on: - pull_request: {} - workflow_dispatch: {} -jobs: - dockerfile-lint: - uses: GeoNet/Actions/.github/workflows/reusable-dockerfile-lint.yml@main - with: - # dockerfiles: | - # ... -``` - -### Container image scan - -STATUS: stable - -Scan a (set of) container image(s) and upload the results to GitHub's Security code scanning center - -Basic usage (non-integrated): - -```yaml -name: scan - -on: - push: {} - workflow_dispatch: {} - -permissions: - security-events: write - -jobs: - scan: - uses: GeoNet/Actions/.github/workflows/reusable-container-image-scan.yml@main - with: - imageRefs: alpine:3.17,postgres:15 -``` - -`inputs.imageRefs` is a comma separated list of container image refs. - -### Terraform management - -STATUS: stable - -Trigger a `terraform plan` (and optionally `terraform apply`) against Terraform located in the repo, starting at the repo root - -Example: - -```yaml -name: terraform - -on: - pull_request: {} - workflow_dispatch: {} - -jobs: - terraform: - uses: GeoNet/Actions/.github/workflows/reusable-terraform-management.yml@main - secrets: inherit - # with: - # allowApply: true -``` - -for Terraform Cloud, set `TF_API_TOKEN` in the repo's Actions Secrets - -for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-terraform-management.yml](.github/workflows/reusable-terraform-management.yml). - -### Presubmit Actions workflow require commit digest vet - -STATUS: stable - -Require Actions to use external actions by their commit digest on presubmit pull requests - -```yaml -name: Presubmit Actions workflow require commit digest vet - -on: - pull_request: - branches: - - main - workflow_dispatch: {} - -jobs: - presubmit-workflow: - uses: GeoNet/Actions/.github/workflows/reusable-presubmit-actions-workflow-require-commit-digest-vet.yml@main -``` - -### Presubmit Go code lint - -STATUS: stable - -Require pull requests for Go projects to have no linting errors - -```yaml -name: Presubmit golangci lint -on: - workflow_dispatch: {} - push: - branches: - - main - - master - - canon - pull_request: {} -jobs: - golangci: - uses: GeoNet/Actions/.github/workflows/reusable-golangci-lint.yml@main - # with: - # config: | - # linters: - # enable: - # - gosec - # - funlen - # - depguard - # - whitespace -``` - -Standard `golangci-lint` config comes from the _.golangc-lint.yml_ file, which is located at the root of a repo and is pulled in with this action. -Whilst not generally recommend, this config can be override per action use, with the `config` input. - -for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-golangci-lint.yml](.github/workflows/reusable-golangci-lint.yml). - -### Go vet - -STATUS: stable - -Run `go vet` against the codebase for static code analysis - -```yaml -name: go vet -on: - push: {} - pull_request: {} - workflow_dispatch: {} -jobs: - go-vet: - uses: GeoNet/Actions/.github/workflows/reusable-go-vet.yml@main -``` - -### Go fmt - -STATUS: stable - -Run `gofmt` against the codebase to format the Go code - -```yaml -name: gofmt -on: - push: {} - pull_request: {} - workflow_dispatch: {} -jobs: - gofmt: - uses: GeoNet/Actions/.github/workflows/reusable-gofmt.yml@main -``` - -### Go test - -STATUS: stable - -Run `go test` against the codebase to run unit tests - -```yaml -name: go test -on: - push: {} - pull_request: {} - workflow_dispatch: {} -jobs: - go-test: - uses: GeoNet/Actions/.github/workflows/reusable-go-test.yml@main - with: - aws-role-arn-to-assume: github-to-s3-upload-role - s3-bucket: my-bucket -``` - -test coverage results upload to job artifacts, found at the bottom of a job summary page. -An optional bucket and role can be provided to upload the results to S3 as well. - -### Go vulnerability check - -STATUS: stable - -Run `govulncheck` against the codebase to scan and report vulnerable packages in use - -```yaml -name: govulncheck -on: - push: {} - pull_request: {} - workflow_dispatch: {} -jobs: - govulncheck: - uses: GeoNet/Actions/.github/workflows/reusable-govulncheck.yml@main -``` +GitHub Actions are limited by organisation policy to those marked Verified on GitHub Marketplace. -### Go build smoke test +Other third-party Actions require review to be allowed or forked. -STATUS: stable +- All external and third-party GitHub Actions MUST be pinned to an immutable full-length commit SHA -Performs `go build -o /dev/null $PATH` to ensure that the programs compile +GitHub Actions can be limited by organisation policy to only allow references pinned to a full-length commit SHA. -Example: +![Require actions to be pinned to a full-length commit SHA](full-length-commit-sha.png) -```yaml -name: go build smoke test - -on: - push: {} - workflow_dispatch: {} - -jobs: - go-build-smoke-test: - uses: GeoNet/Actions/.github/workflows/reusable-go-build-smoke-test.yml@main - # with: - # paths: ./cmd/coolapp -``` - -Note: does not cache or push the binary artifacts anywhere. +A number of supply chain attacks have relied on the version tags values being overwritten with malicious code. -for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-go-build-smoke-test.yml](.github/workflows/reusable-go-build-smoke-test.yml). - -### goimports - -STATUS: stable - -Run `goimports` against the codebase to ensure that the imports are structured correctly +Use the full-length commit SHA. ```yaml -name: goimports -on: - push: {} - pull_request: {} - workflow_dispatch: {} -jobs: - goimports: - uses: GeoNet/Actions/.github/workflows/reusable-goimports.yml@main -``` - -### Presubmit commit policy conformance +# UNSAFE: tag can be moved +- uses: actions/checkout@v4 +- uses: some-org/some-action@main # branch refs are worst of all -STATUS: stable - -Checks commits in PRs for agreed qualities, such as conventionalcommits and style - -```yaml -name: policy conformance -on: - pull_request: - branches: - - main -permissions: - statuses: write - checks: write - contents: read -jobs: - conform: - uses: GeoNet/Actions/.github/workflows/reusable-policy-conformance.yml@main +# SAFE: pinned to immutable SHA +- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 +- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 ``` -each repo where this action is applied must contain a `.conform.yaml` in the root of the repo. -Conform configuration examples: +- Principle of least privilege for access to GITHUB_TOKEN and GitHub Secrets values -- -- -- +Limit workflows that use GitHub Secrets values due to the risk of secrets exposure. -here's an in-line example +Where required define explicit sets of permissions required for a workflow to run. -```yaml -policies: -- type: commit - spec: - dco: true - gpg: - required: true - githubOrganization: GeoNet - spellcheck: - locale: US - maximumOfOneCommit: true - header: - length: 89 - imperative: true - case: lower - invalidLastCharacters: . - body: - required: true - conventional: - types: - - feat - - fix - - nfc - - docs - scopes: [".*"] ``` - -common useful types of requirements: - -- commit signed -- single commit -- commit contains body - -notes: - -- the conventional types include the following types by default and are not needed to be specified - - _feat_ - - _fix_ - - _nfc_ - - _docs_ - -links: - -- -- - -### Go container apps - -STATUS: stable - -a workflow which combines the following workflows - -- ko-build -- go-build-smoke-test -- container-image-scan -- gofmt -- golangci-lint -- go-test -- go-vet -- govulncheck - -```yaml -name: go container apps - -on: - push: {} - pull_request: {} - schedule: - - cron: "0 0 * * *" - release: - types: [published] - workflow_dispatch: {} - -permissions: - actions: read - packages: write - contents: write - pull-requests: write - id-token: write - security-events: write - statuses: write - checks: write +permissions: read-all # deny all write at workflow level jobs: - go-container-apps: - uses: GeoNet/Actions/.github/workflows/reusable-go-container-apps.yml@main - # with: - # registryOverride: string - # paths: string - # imagePromotionConfigLiteral: | - # string - # imagePromotionConfigPath: string - # updateGoVersionAutoMerge: boolean - # containerScanningEnabled: boolean - # containerBuildEnabled: boolean - # registryOverride: $ACCOUNT.dkr.ecr.$REGION.amazonaws.com - # aws-region: ap-southeast-2 - # aws-role-arn-to-assume: arn:aws:iam::$ACCOUNT:role/$ROLE_NAME - # aws-role-duration-seconds: "3600" - # buildSetup: | - # sudo apt install -y something-needed-for-build - # koBuildConfigPath: .ko.yaml -``` - -for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-go-container-apps.yml](.github/workflows/reusable-go-container-apps.yml). - -### Go apps - -STATUS: stable - -a workflow which combines the following workflows - -- go-build-smoke-test -- gofmt -- golangci-lint -- go-test -- go-vet -- update-go-version -- govulncheck + test: + permissions: + contents: read # only needs to read code -```yaml -name: go apps + publish-results: + permissions: + checks: write # needs to write check results + pull-requests: write # needs to comment on PR + contents: read -on: - push: {} - pull_request: {} - schedule: - - cron: "0 0 * * *" release: - types: [published] - workflow_dispatch: {} - -permissions: - actions: read - contents: write - pull-requests: write - id-token: write - security-events: write - statuses: write - checks: write - -jobs: - go-apps: - uses: GeoNet/Actions/.github/workflows/reusable-go-apps.yml@main - # with: - # paths: string - # updateGoVersionAutoMerge: boolean -``` - -for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-go-container-apps.yml](.github/workflows/reusable-go-container-apps.yml). - -### Bash shellcheck - -STATUS: stable - -Runs shellcheck against all known shell scripts - -```yaml -name: bash shellcheck -on: - workflow_dispatch: {} - push: {} - pull_request: {} -jobs: - bash-shellcheck: - uses: GeoNet/Actions/.github/workflows/reusable-bash-shellcheck.yml@main -``` - -### Presubmit README table of contents - -STATUS: stable - -Ensure that the table of contents is updated in README.md, when sections and titles are modified - -```yaml -name: presubmit README table of contents -on: - pull_request: {} - workflow_dispatch: {} -jobs: - presubmit-readme-toc: - uses: GeoNet/Actions/.github/workflows/reusable-presubmit-readme-toc.yml@main -``` - -**important** to note: a markdown file must contain the following - -```text - - - -``` - -given a markdown file (e.g: `README.md`) and the contents above included -in the markdown file, the table of contents can be generated with the command -in that comment: - -```shell -go run sigs.k8s.io/mdtoc@latest --inplace README.md -``` - -note: requires Go to be installed - -### Presubmit GitHub Actions workflow validator - -STATUS: stable - -A workflow to validate all the workflows in the repo - -```yaml -name: presubmit GitHub Actions workflow validator -on: - pull_request: {} - workflow_dispatch: {} -jobs: - presubmit-github-actions-workflow-validator: - uses: GeoNet/Actions/.github/workflows/reusable-presubmit-github-actions-workflow-validator.yml@main -``` - -### GitHub Actions action validator - -STATUS: stable - -A workflow to validate a GitHub action (not reusable workflow) - -```yaml -name: presubmit GitHub Actions action validator -on: - pull_request: {} - workflow_dispatch: {} -jobs: - presubmit-github-actions-action-validator: - uses: GeoNet/Actions/.github/workflows/reusable-github-actions-action-validator.yml@main - with: - actionPaths: ./action.yml -``` - -### Markdown lint - -STATUS: stable - -Lints markdown files - -```yaml -name: lint markdown -on: - pull_request: {} - workflow_dispatch: {} -jobs: - lint-markdown: - uses: GeoNet/Actions/.github/workflows/reusable-markdown-lint.yml@main - # with: - # ignore: some-folder -``` - -### Copy to S3 - -STATUS: stable - -A workflow to copy or sync a local directory to an S3 bucket - -```yaml -name: copy to s3 -on: - push: - branches: - - main - workflow_dispatch: {} -permissions: - id-token: write - contents: read -jobs: - copy-to-s3: - uses: GeoNet/Actions/.github/workflows/reusable-copy-to-s3.yml@main - with: - aws-region: ap-southeast-2 - aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME - aws-role-duration-seconds: 3600 - # aws-role-session-name: - local-source-dir: ./result/ - destination-s3-bucket: s3://some-really-really-cool-s3-bucket - cp-or-sync: sync # 'cp' or 'sync' - direction: to # 'to' or 'from' -``` - -it is also chainable with other jobs - -```yaml -name: copy to s3 -on: - push: - branches: - - main - workflow_dispatch: {} -permissions: - id-token: write - contents: read -jobs: - generate-cool-numbers: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - name: generate - run: | - mkdir -p ./outputs/ - echo "$RANDOM" >> ./outputs/1.txt - echo "$RANDOM" >> ./outputs/1.txt - echo "$RANDOM" >> ./outputs/1.txt - echo "$RANDOM" >> ./outputs/1.txt - echo "$RANDOM" >> ./outputs/2.txt - echo "$RANDOM" >> ./outputs/2.txt - echo "$RANDOM" >> ./outputs/2.txt - echo "$RANDOM" >> ./outputs/2.txt - - name: upload the cool numbers - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: cool-numbers - path: ./outputs/** - retention-days: 1 - copy-to-s3: - needs: generate-cool-numbers - uses: GeoNet/Actions/.github/workflows/reusable-copy-to-s3.yml@main - with: - aws-region: ap-southeast-2 - aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME - aws-role-duration-seconds: 3600 - # aws-role-session-name: - artifact-name: cool-numbers - artifact-path: ./output - destination-s3-bucket: s3://some-really-really-cool-s3-bucket - cp-or-sync: sync # 'cp' or 'sync' - direction: to # 'to' or 'from' -``` - -or copying from S3 - -``` yaml -name: copy from s3 -on: - workflow_dispatch: {} -permissions: - id-token: write - contents: read -jobs: - copy-from-s3: - uses: GeoNet/Actions/.github/workflows/reusable-copy-to-s3.yml@main - with: - aws-region: ap-southeast-2 - aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME - aws-role-duration-seconds: 3600 - # aws-role-session-name: - artifact-name: cool-numbers - artifact-path: ./output - s3-bucket: s3://some-really-really-cool-s3-bucket - cp-or-sync: sync # 'cp' or 'sync' - direction: from # 'to' or 'from' - check-out-the-cool-numbers: - needs: copy-from-s3 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 - with: - name: cool-numbers - path: ./output - - run: | - tree ./output/ -``` - -GitHub Actions artifacts are used to bring state between jobs, this is not possible in any other known way. - -for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-copy-to-s3.yml](.github/workflows/reusable-copy-to-s3.yml). - -### Clean container versions - -STATUS: stable - -```yaml -name: clean-images -permissions: - packages: write -on: - schedule: - - cron: '30 11,23 * * *' - workflow_dispatch: {} -jobs: - clean: - runs-on: ubuntu-latest - uses: GeoNet/Actions/.github/workflows/reusable-clean-containers.yml@main - with: - package-name: base-images/fedora - ignored-regex: '(stable)|(38)' - number-kept: 7 -``` - -### ESLint - -STATUS: beta - -Used to run ESLint on one or more directories. The paths specified -should have a package.json with eslint defined, alongside an eslint config -file named eslint.config.mjs. - -```yaml -name: eslint -on: - push: {} - pull_request: {} - workflow_dispatch: {} -jobs: - eslint: - uses: GeoNet/Actions/.github/workflows/reusable-eslint.yml@main - with: - paths: | - ./root/folder/one - ./cool/root/folder/two - node-version: 22.x -``` - - -### AWS deploy - -STATUS: beta - -CICD-driven container image deployment, using AWS ECR and ECS. - -This workflow supports: - -- ECS service deployments -- EventBridge rule target updates - -No deployment can also be specified, allowing the newly created task revision to be deployed via other mechanisms. - -Example: - -```yaml -name: build-and-deploy - -permissions: - contents: write - id-token: write - -jobs: - # build image - build: - uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main - - # example 1: deploy - ECS service - deploy: - needs: build - uses: GeoNet/Actions/.github/workflows/reusable-aws-deploy.yml@main - with: - aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-geonet-deploy-ROLE_NAME - - # update task definition with new container image uri - task-name: my_task_name - container: my_task_container_name - image: ${{ needs.build.output.image }} - - # deploy - deployment-type: ecs - service: my_service - cluster: my_cluster - - # save deployment information - deployment-tag-param-name: /deployment/my_project/my_service - - # example 2: deploy - EventBridge rule target - deploy: - needs: build - uses: GeoNet/Actions/.github/workflows/reusable-aws-deploy.yml@main - with: - aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-geonet-deploy-ROLE_NAME - - # update task definition with new container image uri - task-name: my_task_name - container: my_task_container_name - image: ${{ needs.build.output.image }} - - # deploy - deployment-type: eventbridge - rule-name: my_rule - - # save deployment information - deployment-tag-param-name: /deployment/my_project/my_service - - # example 3: only create new task revision - deploy: - needs: build - uses: GeoNet/Actions/.github/workflows/reusable-aws-deploy.yml@main - with: - aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-geonet-deploy-ROLE_NAME - - # update task definition with new container image uri - task-name: my_task_name - container: my_task_container_name - image: ${{ needs.build.output.image }} - deployment-type: '' + permissions: + contents: write # only this job needs to push tags/releases + packages: write # and publish packages ``` -The terraform module `gha_iam_ecs_deploy` can be used to setup appropriate permissions for this workflow. -The terraform module `ecs_docker_task_ng` can be used to configure services for use with this workflow, via the `use_cicd_deployment` variable. - -Some example repos using this workflow: `DevTools` and `gloria`. - - -## Composite Actions - -### Tagging - -STATUS: stable - -Generic container tagging. - -Generally will be used in the reusable workflows, but if one needed to use the action directly: - -```yaml -on: [push] - -jobs: - prepare: - runs-on: ubuntu-latest - outputs: - tag: ${{ steps.tagging.outputs.tag }} - steps: - - uses: actions/checkout@v4 - - id: tagging - uses: GeoNet/Actions/.github/actions/tagging@main - build: - needs: prepare - uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main - with: - tag: ${{ needs.prepare.outputs.tag }} -``` - -### Validate bucket URI - -STATUS: beta +- Avoid running any GitHub Actions from forks of repositories -Validate an S3 bucket URI by checking it is in the right format and contains only valid characters. +GitHub repository forks create security exposure for any GitHub Secrets values. +Repository forks should not run GitHub Actions jobs without an approval step. -```yaml -on: [push] - -jobs: - prepare: - runs-on: ubuntu-latest - steps: - - name: Validate bucket - uses: GeoNet/Actions/.github/actions/validate-bucket-uri@main - with: - s3-bucket-uri: s3://my-bucket-to-validate/my-bucket-prefix -``` - -### Copy to S3 - -STATUS: beta - -Copy (or sync) one or more files from GitHub Actions Artifacts to an S3 bucket. - -```yaml -on: [push] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Upload test log to GitHub Artifacts - uses: actions/upload-artifact@v4 - with: - name: test-coverage-results - path: | - /tmp/coverage.out - - name: Upload test log to S3 - uses: GeoNet/Actions/.github/actions/copy-to-s3@main - with: - aws-role-arn-to-assume: my-role - artifact-name: test-coverage-results - artifact-path: ./coverage - s3-bucket-uri: s3://my-bucket/test-coverage-results/ -``` - -### Copy from S3 - -STATUS: beta - -Copy (or sync) one or more files from an S3 bucket to GitHub Actions Artifacts. - -```yaml -on: [push] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Download test log from S3 - uses: GeoNet/Actions/.github/actions/copy-from-s3@main - with: - aws-role-arn-to-assume: my-role - artifact-name: test-coverage-results - artifact-path: ./coverage - s3-bucket-uri: s3://my-bucket/test-coverage-results/coverage.out - single-file: true -``` +- Avoid fields that can be used for script injection attacks -## Other documentation +- Review good practices to mitigate script attacks -### Dependabot and Actions workflow imports +## Dependabot and Actions workflow imports Dependabot is enabled for this repo, see the config in [.github/dependabot.yml](./.github/dependabot.yml). It will automatically update create PRs to update the Actions workflow imports once a week in a seemingly staggered way. To force an update of every external import, run `hack/update-actions-imports.sh` and commit the changes in a new PR. -### Versioning for container images +## Versioning for container images Container registries utilise content addressed storage, meaning to get some data (blob, image), you must request what it's digest is (the process behind tags). When pushing images using the reusable Docker or Ko builds, the images will always be tagged as latest or their digest. @@ -1334,7 +118,7 @@ crane digest IMAGE_REF or in the logs of the workflow run. -### Go Versioning in workflows +## Go Versioning in workflows The default version used in the Go workflows is set to the preceding stable release ("oldstable"). This version will also be supplied as the build argument `CI_GO_IMAGE` for the resuable docker build workflow, allowing users to the ability to diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 00000000..ed69a935 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,1308 @@ + + +- [Actions](#actions) + - [Workflows](#workflows) + - [Ko build](#ko-build) + - [Docker build](#docker-build) + - [Dockerfile lint](#dockerfile-lint) + - [Container image scan](#container-image-scan) + - [Terraform management](#terraform-management) + - [Presubmit Actions workflow require commit digest vet](#presubmit-actions-workflow-require-commit-digest-vet) + - [Presubmit Go code lint](#presubmit-go-code-lint) + - [Go vet](#go-vet) + - [Go fmt](#go-fmt) + - [Go test](#go-test) + - [Go vulnerability check](#go-vulnerability-check) + - [Go build smoke test](#go-build-smoke-test) + - [goimports](#goimports) + - [Presubmit commit policy conformance](#presubmit-commit-policy-conformance) + - [Go container apps](#go-container-apps) + - [Go apps](#go-apps) + - [Bash shellcheck](#bash-shellcheck) + - [Presubmit README table of contents](#presubmit-readme-table-of-contents) + - [Presubmit GitHub Actions workflow validator](#presubmit-github-actions-workflow-validator) + - [GitHub Actions action validator](#github-actions-action-validator) + - [Markdown lint](#markdown-lint) + - [Copy to S3](#copy-to-s3) + - [Clean container versions](#clean-container-versions) + - [ESLint](#eslint) + - [AWS deploy](#aws-deploy) + - [Composite Actions](#composite-actions) + - [Tagging](#tagging) + - [Validate bucket URI](#validate-bucket-uri) + - [Copy to S3](#copy-to-s3-1) + - [Copy from S3](#copy-from-s3) + + +# Actions + +> reusable GitHub actions across several projects + +This repo is for reusable workflows to run in GitHub Actions for the GeoNet program. +The workflows are not publicly supported and come with absolutely no warranty. + +## Workflows + +There are three types of workflows in this repo + +- reusable: GeoNet downstream implementations of existing actions or common patterns +- reusable _apps_: combined function workflows which include several other reusable workflows +- GeoNet/Actions maintainability: workflows which support the consistency of the workflows in this repo + +the workflows are intended to work with and around the maintainers of GeoNet software for automations which are valuable to the project. + + + +### Ko build + +STATUS: stable + +Generic build for containerised Go applications with [Ko](https://ko.build). + +Example: + +```yaml +name: build + +on: + push: {} + release: + types: [published] + workflow_dispatch: {} + +permissions: + packages: write + id-token: write + +jobs: + build: + uses: GeoNet/Actions/.github/workflows/reusable-ko-build.yml@main + # with: + # paths: ./cmd/coolapp + # registryOverride: registry.example.com + # registryGhcrUsernameOverride: ${{ secrets.GHCR_USERNAME }} + # registryGhcrPasswordOverride: ${{ secrets.GHCR_PASSWORD }} + # push: true + # aws-region: ap-southeast-2 + # aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME + # aws-role-duration-seconds: "3600" + # setup: | + # sudo apt install -y something-needed-for-build + # configPath: .ko.yaml +``` + +- dynamic build of images based on entrypoints (where there is a `package main`), unless if _inputs.paths_ is set +- fast! + +Pushing to ECR example: + +```yaml +name: build + +on: + push: {} + release: + types: [published] + workflow_dispatch: {} + +permissions: + packages: write + id-token: write + +jobs: + build: + uses: GeoNet/Actions/.github/workflows/reusable-ko-build.yml@main + with: + registryOverride: $ACCOUNT.dkr.ecr.$REGION.amazonaws.com + aws-region: ap-southeast-2 + aws-role-arn-to-assume: arn:aws:iam::$ACCOUNT:role/$ROLE_NAME + aws-role-duration-seconds: "3600" +``` + +for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-ko-build.yml](.github/workflows/reusable-ko-build.yml). + +### Docker build + +STATUS: stable + +Generic container image build with Docker. + +Single use example: + +```yaml +name: build + +on: + push: {} + release: + types: [published] + workflow_dispatch: {} + +permissions: + packages: write + id-token: write + +jobs: + build: + uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main + with: + context: . + dockerfile: ./Dockerfile + imageName: cool + platforms: 'linux/amd64,linux/arm64' + push: ${{ github.ref == 'refs/heads/main' }} + buildArgs: | + VERSION=${{ github.sha }} +``` + +to add more, copy the block like `jobs.build` and replace values in next block where desired. + +Multiple dynamic parallel builds based on directory subfolders: + +```yaml +name: build + +on: + push: {} + release: + types: [published] + workflow_dispatch: {} + +permissions: + packages: write + id-token: write + +env: + FOLDER: apps + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set.outputs.matrix }} + steps: + - uses: actions/checkout@v3 + - uses: GeoNet/yq@bbe305500687a5fe8498d74883c17f0f06431ac4 # master + - id: set + run: | + echo "matrix=$(find $FOLDER -mindepth 1 -maxdepth 1 -type d | xargs -n 1 basename | xargs | yq 'split(" ")|.[]|{"target":.}' -ojson | jq -rcM -s .)" >> $GITHUB_OUTPUT + - name: check output + run: | + jq . <<< '${{ steps.set.outputs.matrix }}' + + build: + needs: prepare + strategy: + matrix: + include: ${{ fromJSON(needs.prepare.outputs.matrix) }} + uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main + with: + context: apps/${{ fromJSON(toJSON(matrix)).target }} + dockerfile: apps/${{ fromJSON(toJSON(matrix)).target }}/Dockerfile + imageName: ${{ fromJSON(toJSON(matrix)).target }} + platforms: 'linux/amd64,linux/arm64' +``` + +Pushing to ECR example: + +```yaml +name: build + +on: + push: {} + release: + types: [published] + workflow_dispatch: {} + +permissions: + packages: write + id-token: write + +jobs: + build: + uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main + with: + context: . + dockerfile: ./Dockerfile + imageName: cool + platforms: 'linux/amd64,linux/arm64' + push: ${{ github.ref == 'refs/heads/main' }} + registryOverride: $ACCOUNT.dkr.ecr.$REGION.amazonaws.com + aws-region: ap-southeast-2 + aws-role-arn-to-assume: arn:aws:iam::$ACCOUNT:role/$ROLE_NAME + aws-role-duration-seconds: "3600" +``` + +Pulling in a GitHub artifact for a build: + +```yaml +name: build + +on: + push: {} + release: + types: [published] + workflow_dispatch: {} + +permissions: + packages: write + id-token: write + +jobs: + prepare: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - run: | + mkdir -p ./apps/cool-ng/assets + echo 'hello!' > ./apps/cool-ng/assets/index.html + - name: upload the cool numbers + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: cool-ng + path: ./apps/cool-ng/assets + retention-days: 1 + build: + needs: prepare + uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main + with: + context: ./apps/cool-ng + dockerfile: ./apps/cool-ng/Dockerfile + imageName: cool-ng + platforms: 'linux/amd64,linux/arm64' + push: ${{ github.ref == 'refs/heads/main' }} + artifact-name: cool-ng + artifact-path: ./apps/cool-ng/assets +``` + +Pulling in things from S3 in a build: + +```yaml +name: build + +on: + push: {} + release: + types: [published] + workflow_dispatch: {} + +permissions: + packages: write + id-token: write + +jobs: + copy-from-s3: + uses: GeoNet/Actions/.github/workflows/reusable-copy-to-s3.yml@main + with: + aws-region: ap-southeast-2 + aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME + aws-role-duration-seconds: 3600 + artifact-name: cool-ng + artifact-path: ./apps/cool-ng/assets + s3-bucket: s3://some-really-really-cool-s3-bucket/assets + cp-or-sync: cp + direction: from # 'to' or 'from' + build: + needs: copy-from-s3 + uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main + with: + context: ./apps/cool-ng + dockerfile: ./apps/cool-ng/Dockerfile + imageName: cool-ng + platforms: 'linux/amd64,linux/arm64' + push: ${{ github.ref == 'refs/heads/main' }} + artifact-name: cool-ng + artifact-path: ./apps/cool-ng/assets +``` + +note: $registryOverride + '/' + $imageName must be an existing ECR + +Override auth to ghcr.io + +```yaml +name: build + +on: + push: {} + release: + types: [published] + workflow_dispatch: {} + +permissions: + packages: write + id-token: write + +jobs: + build: + uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main + with: + context: . + dockerfile: ./Dockerfile + imageName: cool + platforms: 'linux/amd64,linux/arm64' + push: ${{ github.ref == 'refs/heads/main' }} + buildArgs: | + VERSION=${{ github.sha }} + registryGhcrUsernameOverride: example + secrets: inherit +``` + +Copy an image to a different container registry: + +```yaml +name: build + +on: + push: {} + release: + types: [published] + workflow_dispatch: {} + +permissions: + packages: write + id-token: write + +env: + VERSION_CRANE: v0.16.1 + +jobs: + build: + uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main + with: + context: . + dockerfile: ./Dockerfile + imageName: cool + platforms: 'linux/amd64,linux/arm64' + push: ${{ github.ref == 'refs/heads/main' }} + copy-image-to-registry: + needs: build + runs-on: ubuntu-latest + steps: + - uses: GeoNet/setup-crane@00c9e93efa4e1138c9a7a5c594acd6c75a2fbf0c # main + with: + version: ${{ env.VERSION_CRANE }} + - name: authenticate to registry + run: | + echo SOME_PASSWORD | crane auth login -u some-user --password-stdin + - name: copy image + env: + SOURCE: ${{ needs.build.outputs.image }} + DESTINATION: ghcr.io/someorg/someimage:sometag + run: | + crane cp "$SOURCE" "$DESTINATION" +``` + +this may be useful for things like image promotion or staging. + +for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-docker-build.yml](.github/workflows/reusable-docker-build.yml). + +### Dockerfile lint + +STATUS: stable + +```yaml +name: dockerfile lint +on: + pull_request: {} + workflow_dispatch: {} +jobs: + dockerfile-lint: + uses: GeoNet/Actions/.github/workflows/reusable-dockerfile-lint.yml@main + with: + # dockerfiles: | + # ... +``` + +### Container image scan + +STATUS: stable + +Scan a (set of) container image(s) and upload the results to GitHub's Security code scanning center + +Basic usage (non-integrated): + +```yaml +name: scan + +on: + push: {} + workflow_dispatch: {} + +permissions: + security-events: write + +jobs: + scan: + uses: GeoNet/Actions/.github/workflows/reusable-container-image-scan.yml@main + with: + imageRefs: alpine:3.17,postgres:15 +``` + +`inputs.imageRefs` is a comma separated list of container image refs. + +### Terraform management + +STATUS: stable + +Trigger a `terraform plan` (and optionally `terraform apply`) against Terraform located in the repo, starting at the repo root + +Example: + +```yaml +name: terraform + +on: + pull_request: {} + workflow_dispatch: {} + +jobs: + terraform: + uses: GeoNet/Actions/.github/workflows/reusable-terraform-management.yml@main + secrets: inherit + # with: + # allowApply: true +``` + +for Terraform Cloud, set `TF_API_TOKEN` in the repo's Actions Secrets + +for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-terraform-management.yml](.github/workflows/reusable-terraform-management.yml). + +### Presubmit Actions workflow require commit digest vet + +STATUS: stable + +Require Actions to use external actions by their commit digest on presubmit pull requests + +```yaml +name: Presubmit Actions workflow require commit digest vet + +on: + pull_request: + branches: + - main + workflow_dispatch: {} + +jobs: + presubmit-workflow: + uses: GeoNet/Actions/.github/workflows/reusable-presubmit-actions-workflow-require-commit-digest-vet.yml@main +``` + +### Presubmit Go code lint + +STATUS: stable + +Require pull requests for Go projects to have no linting errors + +```yaml +name: Presubmit golangci lint +on: + workflow_dispatch: {} + push: + branches: + - main + - master + - canon + pull_request: {} +jobs: + golangci: + uses: GeoNet/Actions/.github/workflows/reusable-golangci-lint.yml@main + # with: + # config: | + # linters: + # enable: + # - gosec + # - funlen + # - depguard + # - whitespace +``` + +Standard `golangci-lint` config comes from the _.golangc-lint.yml_ file, which is located at the root of a repo and is pulled in with this action. +Whilst not generally recommend, this config can be override per action use, with the `config` input. + +for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-golangci-lint.yml](.github/workflows/reusable-golangci-lint.yml). + +### Go vet + +STATUS: stable + +Run `go vet` against the codebase for static code analysis + +```yaml +name: go vet +on: + push: {} + pull_request: {} + workflow_dispatch: {} +jobs: + go-vet: + uses: GeoNet/Actions/.github/workflows/reusable-go-vet.yml@main +``` + +### Go fmt + +STATUS: stable + +Run `gofmt` against the codebase to format the Go code + +```yaml +name: gofmt +on: + push: {} + pull_request: {} + workflow_dispatch: {} +jobs: + gofmt: + uses: GeoNet/Actions/.github/workflows/reusable-gofmt.yml@main +``` + +### Go test + +STATUS: stable + +Run `go test` against the codebase to run unit tests + +```yaml +name: go test +on: + push: {} + pull_request: {} + workflow_dispatch: {} +jobs: + go-test: + uses: GeoNet/Actions/.github/workflows/reusable-go-test.yml@main + with: + aws-role-arn-to-assume: github-to-s3-upload-role + s3-bucket: my-bucket +``` + +test coverage results upload to job artifacts, found at the bottom of a job summary page. +An optional bucket and role can be provided to upload the results to S3 as well. + +### Go vulnerability check + +STATUS: stable + +Run `govulncheck` against the codebase to scan and report vulnerable packages in use + +```yaml +name: govulncheck +on: + push: {} + pull_request: {} + workflow_dispatch: {} +jobs: + govulncheck: + uses: GeoNet/Actions/.github/workflows/reusable-govulncheck.yml@main +``` + +### Go build smoke test + +STATUS: stable + +Performs `go build -o /dev/null $PATH` to ensure that the programs compile + +Example: + +```yaml +name: go build smoke test + +on: + push: {} + workflow_dispatch: {} + +jobs: + go-build-smoke-test: + uses: GeoNet/Actions/.github/workflows/reusable-go-build-smoke-test.yml@main + # with: + # paths: ./cmd/coolapp +``` + +Note: does not cache or push the binary artifacts anywhere. + +for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-go-build-smoke-test.yml](.github/workflows/reusable-go-build-smoke-test.yml). + +### goimports + +STATUS: stable + +Run `goimports` against the codebase to ensure that the imports are structured correctly + +```yaml +name: goimports +on: + push: {} + pull_request: {} + workflow_dispatch: {} +jobs: + goimports: + uses: GeoNet/Actions/.github/workflows/reusable-goimports.yml@main +``` + +### Presubmit commit policy conformance + +STATUS: stable + +Checks commits in PRs for agreed qualities, such as conventionalcommits and style + +```yaml +name: policy conformance +on: + pull_request: + branches: + - main +permissions: + statuses: write + checks: write + contents: read +jobs: + conform: + uses: GeoNet/Actions/.github/workflows/reusable-policy-conformance.yml@main +``` + +each repo where this action is applied must contain a `.conform.yaml` in the root of the repo. +Conform configuration examples: + +- +- +- + +here's an in-line example + +```yaml +policies: +- type: commit + spec: + dco: true + gpg: + required: true + githubOrganization: GeoNet + spellcheck: + locale: US + maximumOfOneCommit: true + header: + length: 89 + imperative: true + case: lower + invalidLastCharacters: . + body: + required: true + conventional: + types: + - feat + - fix + - nfc + - docs + scopes: [".*"] +``` + +common useful types of requirements: + +- commit signed +- single commit +- commit contains body + +notes: + +- the conventional types include the following types by default and are not needed to be specified + - _feat_ + - _fix_ + - _nfc_ + - _docs_ + +links: + +- +- + +### Go container apps + +STATUS: stable + +a workflow which combines the following workflows + +- ko-build +- go-build-smoke-test +- container-image-scan +- gofmt +- golangci-lint +- go-test +- go-vet +- govulncheck + +```yaml +name: go container apps + +on: + push: {} + pull_request: {} + schedule: + - cron: "0 0 * * *" + release: + types: [published] + workflow_dispatch: {} + +permissions: + actions: read + packages: write + contents: write + pull-requests: write + id-token: write + security-events: write + statuses: write + checks: write + +jobs: + go-container-apps: + uses: GeoNet/Actions/.github/workflows/reusable-go-container-apps.yml@main + # with: + # registryOverride: string + # paths: string + # imagePromotionConfigLiteral: | + # string + # imagePromotionConfigPath: string + # updateGoVersionAutoMerge: boolean + # containerScanningEnabled: boolean + # containerBuildEnabled: boolean + # registryOverride: $ACCOUNT.dkr.ecr.$REGION.amazonaws.com + # aws-region: ap-southeast-2 + # aws-role-arn-to-assume: arn:aws:iam::$ACCOUNT:role/$ROLE_NAME + # aws-role-duration-seconds: "3600" + # buildSetup: | + # sudo apt install -y something-needed-for-build + # koBuildConfigPath: .ko.yaml +``` + +for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-go-container-apps.yml](.github/workflows/reusable-go-container-apps.yml). + +### Go apps + +STATUS: stable + +a workflow which combines the following workflows + +- go-build-smoke-test +- gofmt +- golangci-lint +- go-test +- go-vet +- update-go-version +- govulncheck + +```yaml +name: go apps + +on: + push: {} + pull_request: {} + schedule: + - cron: "0 0 * * *" + release: + types: [published] + workflow_dispatch: {} + +permissions: + actions: read + contents: write + pull-requests: write + id-token: write + security-events: write + statuses: write + checks: write + +jobs: + go-apps: + uses: GeoNet/Actions/.github/workflows/reusable-go-apps.yml@main + # with: + # paths: string + # updateGoVersionAutoMerge: boolean +``` + +for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-go-container-apps.yml](.github/workflows/reusable-go-container-apps.yml). + +### Bash shellcheck + +STATUS: stable + +Runs shellcheck against all known shell scripts + +```yaml +name: bash shellcheck +on: + workflow_dispatch: {} + push: {} + pull_request: {} +jobs: + bash-shellcheck: + uses: GeoNet/Actions/.github/workflows/reusable-bash-shellcheck.yml@main +``` + +### Presubmit README table of contents + +STATUS: stable + +Ensure that the table of contents is updated in README.md, when sections and titles are modified + +```yaml +name: presubmit README table of contents +on: + pull_request: {} + workflow_dispatch: {} +jobs: + presubmit-readme-toc: + uses: GeoNet/Actions/.github/workflows/reusable-presubmit-readme-toc.yml@main +``` + +**important** to note: a markdown file must contain the following + +```text + + + +``` + +given a markdown file (e.g: `README.md`) and the contents above included +in the markdown file, the table of contents can be generated with the command +in that comment: + +```shell +go run sigs.k8s.io/mdtoc@latest --inplace README.md +``` + +note: requires Go to be installed + +### Presubmit GitHub Actions workflow validator + +STATUS: stable + +A workflow to validate all the workflows in the repo + +```yaml +name: presubmit GitHub Actions workflow validator +on: + pull_request: {} + workflow_dispatch: {} +jobs: + presubmit-github-actions-workflow-validator: + uses: GeoNet/Actions/.github/workflows/reusable-presubmit-github-actions-workflow-validator.yml@main +``` + +### GitHub Actions action validator + +STATUS: stable + +A workflow to validate a GitHub action (not reusable workflow) + +```yaml +name: presubmit GitHub Actions action validator +on: + pull_request: {} + workflow_dispatch: {} +jobs: + presubmit-github-actions-action-validator: + uses: GeoNet/Actions/.github/workflows/reusable-github-actions-action-validator.yml@main + with: + actionPaths: ./action.yml +``` + +### Markdown lint + +STATUS: stable + +Lints markdown files + +```yaml +name: lint markdown +on: + pull_request: {} + workflow_dispatch: {} +jobs: + lint-markdown: + uses: GeoNet/Actions/.github/workflows/reusable-markdown-lint.yml@main + # with: + # ignore: some-folder +``` + +### Copy to S3 + +STATUS: stable + +A workflow to copy or sync a local directory to an S3 bucket + +```yaml +name: copy to s3 +on: + push: + branches: + - main + workflow_dispatch: {} +permissions: + id-token: write + contents: read +jobs: + copy-to-s3: + uses: GeoNet/Actions/.github/workflows/reusable-copy-to-s3.yml@main + with: + aws-region: ap-southeast-2 + aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME + aws-role-duration-seconds: 3600 + # aws-role-session-name: + local-source-dir: ./result/ + destination-s3-bucket: s3://some-really-really-cool-s3-bucket + cp-or-sync: sync # 'cp' or 'sync' + direction: to # 'to' or 'from' +``` + +it is also chainable with other jobs + +```yaml +name: copy to s3 +on: + push: + branches: + - main + workflow_dispatch: {} +permissions: + id-token: write + contents: read +jobs: + generate-cool-numbers: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - name: generate + run: | + mkdir -p ./outputs/ + echo "$RANDOM" >> ./outputs/1.txt + echo "$RANDOM" >> ./outputs/1.txt + echo "$RANDOM" >> ./outputs/1.txt + echo "$RANDOM" >> ./outputs/1.txt + echo "$RANDOM" >> ./outputs/2.txt + echo "$RANDOM" >> ./outputs/2.txt + echo "$RANDOM" >> ./outputs/2.txt + echo "$RANDOM" >> ./outputs/2.txt + - name: upload the cool numbers + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: cool-numbers + path: ./outputs/** + retention-days: 1 + copy-to-s3: + needs: generate-cool-numbers + uses: GeoNet/Actions/.github/workflows/reusable-copy-to-s3.yml@main + with: + aws-region: ap-southeast-2 + aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME + aws-role-duration-seconds: 3600 + # aws-role-session-name: + artifact-name: cool-numbers + artifact-path: ./output + destination-s3-bucket: s3://some-really-really-cool-s3-bucket + cp-or-sync: sync # 'cp' or 'sync' + direction: to # 'to' or 'from' +``` + +or copying from S3 + +``` yaml +name: copy from s3 +on: + workflow_dispatch: {} +permissions: + id-token: write + contents: read +jobs: + copy-from-s3: + uses: GeoNet/Actions/.github/workflows/reusable-copy-to-s3.yml@main + with: + aws-region: ap-southeast-2 + aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ROLE_NAME + aws-role-duration-seconds: 3600 + # aws-role-session-name: + artifact-name: cool-numbers + artifact-path: ./output + s3-bucket: s3://some-really-really-cool-s3-bucket + cp-or-sync: sync # 'cp' or 'sync' + direction: from # 'to' or 'from' + check-out-the-cool-numbers: + needs: copy-from-s3 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: cool-numbers + path: ./output + - run: | + tree ./output/ +``` + +GitHub Actions artifacts are used to bring state between jobs, this is not possible in any other known way. + +for configuration see [`on.workflow_call.inputs` in .github/workflows/reusable-copy-to-s3.yml](.github/workflows/reusable-copy-to-s3.yml). + +### Clean container versions + +STATUS: stable + +```yaml +name: clean-images +permissions: + packages: write +on: + schedule: + - cron: '30 11,23 * * *' + workflow_dispatch: {} +jobs: + clean: + runs-on: ubuntu-latest + uses: GeoNet/Actions/.github/workflows/reusable-clean-containers.yml@main + with: + package-name: base-images/fedora + ignored-regex: '(stable)|(38)' + number-kept: 7 +``` + +### ESLint + +STATUS: beta + +Used to run ESLint on one or more directories. The paths specified +should have a package.json with eslint defined, alongside an eslint config +file named eslint.config.mjs. + +```yaml +name: eslint +on: + push: {} + pull_request: {} + workflow_dispatch: {} +jobs: + eslint: + uses: GeoNet/Actions/.github/workflows/reusable-eslint.yml@main + with: + paths: | + ./root/folder/one + ./cool/root/folder/two + node-version: 22.x +``` + + +### AWS deploy + +STATUS: beta + +CICD-driven container image deployment, using AWS ECR and ECS. + +This workflow supports: + +- ECS service deployments +- EventBridge rule target updates + +No deployment can also be specified, allowing the newly created task revision to be deployed via other mechanisms. + +Example: + +```yaml +name: build-and-deploy + +permissions: + contents: write + id-token: write + +jobs: + # build image + build: + uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main + + # example 1: deploy - ECS service + deploy: + needs: build + uses: GeoNet/Actions/.github/workflows/reusable-aws-deploy.yml@main + with: + aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-geonet-deploy-ROLE_NAME + + # update task definition with new container image uri + task-name: my_task_name + container: my_task_container_name + image: ${{ needs.build.output.image }} + + # deploy + deployment-type: ecs + service: my_service + cluster: my_cluster + + # save deployment information + deployment-tag-param-name: /deployment/my_project/my_service + + # example 2: deploy - EventBridge rule target + deploy: + needs: build + uses: GeoNet/Actions/.github/workflows/reusable-aws-deploy.yml@main + with: + aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-geonet-deploy-ROLE_NAME + + # update task definition with new container image uri + task-name: my_task_name + container: my_task_container_name + image: ${{ needs.build.output.image }} + + # deploy + deployment-type: eventbridge + rule-name: my_rule + + # save deployment information + deployment-tag-param-name: /deployment/my_project/my_service + + # example 3: only create new task revision + deploy: + needs: build + uses: GeoNet/Actions/.github/workflows/reusable-aws-deploy.yml@main + with: + aws-role-arn-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-geonet-deploy-ROLE_NAME + + # update task definition with new container image uri + task-name: my_task_name + container: my_task_container_name + image: ${{ needs.build.output.image }} + deployment-type: '' +``` + +The terraform module `gha_iam_ecs_deploy` can be used to setup appropriate permissions for this workflow. +The terraform module `ecs_docker_task_ng` can be used to configure services for use with this workflow, via the `use_cicd_deployment` variable. + +Some example repos using this workflow: `DevTools` and `gloria`. + + +## Composite Actions + +### Tagging + +STATUS: stable + +Generic container tagging. + +Generally will be used in the reusable workflows, but if one needed to use the action directly: + +```yaml +on: [push] + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tagging.outputs.tag }} + steps: + - uses: actions/checkout@v4 + - id: tagging + uses: GeoNet/Actions/.github/actions/tagging@main + build: + needs: prepare + uses: GeoNet/Actions/.github/workflows/reusable-docker-build.yml@main + with: + tag: ${{ needs.prepare.outputs.tag }} +``` + +### Validate bucket URI + +STATUS: beta + +Validate an S3 bucket URI by checking it is in the right format and contains only valid characters. + +```yaml +on: [push] + +jobs: + prepare: + runs-on: ubuntu-latest + steps: + - name: Validate bucket + uses: GeoNet/Actions/.github/actions/validate-bucket-uri@main + with: + s3-bucket-uri: s3://my-bucket-to-validate/my-bucket-prefix +``` + +### Copy to S3 + +STATUS: beta + +Copy (or sync) one or more files from GitHub Actions Artifacts to an S3 bucket. + +```yaml +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Upload test log to GitHub Artifacts + uses: actions/upload-artifact@v4 + with: + name: test-coverage-results + path: | + /tmp/coverage.out + - name: Upload test log to S3 + uses: GeoNet/Actions/.github/actions/copy-to-s3@main + with: + aws-role-arn-to-assume: my-role + artifact-name: test-coverage-results + artifact-path: ./coverage + s3-bucket-uri: s3://my-bucket/test-coverage-results/ +``` + +### Copy from S3 + +STATUS: beta + +Copy (or sync) one or more files from an S3 bucket to GitHub Actions Artifacts. + +```yaml +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Download test log from S3 + uses: GeoNet/Actions/.github/actions/copy-from-s3@main + with: + aws-role-arn-to-assume: my-role + artifact-name: test-coverage-results + artifact-path: ./coverage + s3-bucket-uri: s3://my-bucket/test-coverage-results/coverage.out + single-file: true +``` diff --git a/full-length-commit-sha.png b/full-length-commit-sha.png new file mode 100644 index 0000000000000000000000000000000000000000..ea4866b94bd71c1152330e8b18db590b9d4d8bd2 GIT binary patch literal 7745 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 literal 0 HcmV?d00001 diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..5ab3c3a6 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +go = "1.26.1"