diff --git a/.editorconfig b/.editorconfig index 42b5ed3..f8fec28 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,12 +2,14 @@ root = true [*] end_of_line = lf +insert_final_newline = true indent_size = 2 indent_style = space -insert_final_newline = true [*.{js,json,tf*,yml,yaml}] +max_line_length = 180 trim_trailing_whitespace = true [*.md] +max_line_length = 240 trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c814d7e..7e4956e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,10 @@ on: branches: [main] workflow_dispatch: {} +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read diff --git a/.github/workflows/reusable-container-publication.yml b/.github/workflows/reusable-container-publication.yml index cdad238..396388d 100644 --- a/.github/workflows/reusable-container-publication.yml +++ b/.github/workflows/reusable-container-publication.yml @@ -107,28 +107,31 @@ jobs: repository: devpro/github-workflow-parts ref: ${{ inputs.workflow-parts-version }} path: workflow-parts + # The Docker CLI of the runner does what the docker/* actions wrapped, with no third party action in between. - name: Login to container registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee - with: - registry: ${{ inputs.container-registry }} - username: ${{ secrets.container-registry-username }} - password: ${{ secrets.container-registry-password }} + env: + REGISTRY: ${{ inputs.container-registry }} + REGISTRY_USERNAME: ${{ secrets.container-registry-username }} + REGISTRY_PASSWORD: ${{ secrets.container-registry-password }} + run: echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" --username "$REGISTRY_USERNAME" --password-stdin - name: Set up QEMU - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 + # Emulation is only needed for a platform other than the runner's own. + if: ${{ inputs.image-platform != 'linux/amd64' }} + run: docker run --privileged --rm tonistiigi/binfmt --install all - name: Set up Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 + run: docker buildx create --use --driver docker-container - name: Build and push container image - id: build-push - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf - with: - context: ${{ inputs.working-directory }} - file: ${{ inputs.image-definition }} - platforms: ${{ inputs.image-platform }} - push: true - tags: ${{ env.IMAGE_REF }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: ${{ inputs.extra-build-arguments }} + # The gha cache backend is reachable from JavaScript actions only, so the build runs without a cache. + env: + IMAGE_DEFINITION: ${{ inputs.image-definition }} + IMAGE_PLATFORM: ${{ inputs.image-platform }} + BUILD_ARGUMENTS: ${{ inputs.extra-build-arguments }} + run: | + args=(--file "$IMAGE_DEFINITION" --platform "$IMAGE_PLATFORM" --tag "$IMAGE_REF" --push) + while IFS= read -r argument; do + [ -n "$argument" ] && args+=(--build-arg "$argument") + done <<< "$BUILD_ARGUMENTS" + docker buildx build "${args[@]}" . - name: Generate SBOM with Syft uses: ./workflow-parts/actions/syft/generate-sbom continue-on-error: true diff --git a/.github/workflows/reusable-dotnet-quality.yml b/.github/workflows/reusable-dotnet-quality.yml index 2f31708..786b27d 100644 --- a/.github/workflows/reusable-dotnet-quality.yml +++ b/.github/workflows/reusable-dotnet-quality.yml @@ -169,8 +169,7 @@ jobs: sonar-token: ${{ secrets.sonar-token }} - name: Check license compliance with FOSSA if: ${{ inputs.fossa-enabled }} - uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 - id: fossa + uses: ./workflow-parts/actions/fossa/analyze # https://status.fossa.com/ continue-on-error: true timeout-minutes: 3 @@ -178,11 +177,7 @@ jobs: api-key: "${{ secrets.fossa-api-key }}" run-tests: ${{ inputs.fossa-test && github.event_name == 'pull_request' }} test-diff-revision: ${{ github.event.pull_request.base.sha }} - generate-report: html - - name: Create FOSSA report file - if: ${{ inputs.fossa-enabled && steps.fossa.outcome == 'success' }} - run: echo '${{ steps.fossa.outputs.report }}' > report/fossa.html - continue-on-error: true + report-file: report/fossa.html - name: Generate SBOM with Syft uses: ./workflow-parts/actions/syft/generate-sbom continue-on-error: true diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index dae97fc..478087b 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -1,6 +1,7 @@ gitignore: true ignores: - - "**/node_modules/**" + - node_modules + - CLAUDE.md config: # ref. https://github.com/DavidAnson/markdownlint default: true diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..93e9423 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,27 @@ +# github-workflow-parts: agent context + +GitHub workflow actions and reusable workflows. + +Linux or WSL2, Docker and `bash`. + +## Working rules + +- **Every command runs in the foreground, and the agent waits for it.** + No background commands, no subagents, no forks, no parallel tasks, even for a long commands. +- Headers stay: short documentation still has sections. +- Linters for YAML and Markdown are never run by an agent. +- Commit only when asked, and never push. + Shell scripts are `snake_case` and committed with the executable bit (`git update-index --chmod=+x`). +- Documentation is as short as possible. + +## Writing style + +Applies to Markdown, code comments, commit messages and prose in scripts. + +- **A comment says why, not what, and the why is timeless.** +- **One thought per line.** + Every sentence starts on its own line, and there is no maximum line length. +- **No em dash, no en dash.** + A colon, a comma, or a full stop. +- **No second person.** + "The working tree", not "your working tree"; ``, not ``. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/actions/cosign/sign/action.yml b/actions/cosign/sign/action.yml index 3316f46..f754b0e 100644 --- a/actions/cosign/sign/action.yml +++ b/actions/cosign/sign/action.yml @@ -31,6 +31,8 @@ runs: steps: - name: Install Cosign run: | + # Downloaded into a directory of its own, since the repository may hold a file or a directory named like the binary. + cd "$(mktemp -d)" echo "Downloading Cosign binary and checksums..." curl -sL -O https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/${COSIGN_BINARY} curl -sL -O "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/${COSIGN_CHECKSUM_FILE}" @@ -42,7 +44,11 @@ runs: echo "Verification successful!" rm $COSIGN_CHECKSUM_FILE chmod +x $COSIGN_BINARY - sudo mv $COSIGN_BINARY /usr/local/bin/cosign + # Installed where the job owns the directory, so neither root nor sudo is needed, on a hosted runner or in a container. + mkdir -p "${RUNNER_TEMP}/bin" + mv $COSIGN_BINARY "${RUNNER_TEMP}/bin/cosign" + echo "${RUNNER_TEMP}/bin" >> "$GITHUB_PATH" + export PATH="${RUNNER_TEMP}/bin:$PATH" cosign version else echo "ERROR: Checksum verification failed!" >&2 diff --git a/actions/dotnet/install-lint-restore/action.yml b/actions/dotnet/install-lint-restore/action.yml index a595889..c1fa6ee 100644 --- a/actions/dotnet/install-lint-restore/action.yml +++ b/actions/dotnet/install-lint-restore/action.yml @@ -21,7 +21,7 @@ runs: - name: Install .NET tools run: | dotnet tool install --global dotnet-reportgenerator-globaltool - export PATH="$PATH:/root/.dotnet/tools" + echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" shell: bash - name: Restore .NET packages run: dotnet restore diff --git a/actions/fossa/analyze/action.yml b/actions/fossa/analyze/action.yml new file mode 100644 index 0000000..4fa96c2 --- /dev/null +++ b/actions/fossa/analyze/action.yml @@ -0,0 +1,93 @@ +name: Analyze with FOSSA +description: | + Installs the FOSSA CLI by downloading the official binary from its GitHub release and verifying its SHA256 checksum, + then analyzes the dependencies for license compliance, optionally tests them against the policy, and writes an HTML report + (replacement for fossas/fossa-action, to reduce third-party GitHub Action supply-chain risk). + Linux runners only (for example ubuntu-latest). + +inputs: + fossa-version: + description: Version of the FOSSA CLI to install (check latest from https://github.com/fossas/fossa-cli/releases) + required: false + default: "3.19.3" + api-key: + description: FOSSA API key + required: true + run-tests: + description: Test the analyzed dependencies against the FOSSA policy + required: false + default: "false" + test-diff-revision: + description: Revision to report only the issues introduced since (empty tests everything) + required: false + default: "" + report-file: + description: Path of the generated HTML report (empty writes none) + required: false + default: "report/fossa.html" + +runs: + using: "composite" + steps: + - name: Install FOSSA CLI + shell: bash + env: + FOSSA_VERSION: ${{ inputs.fossa-version }} + FOSSA_TARBALL: fossa_${{ inputs.fossa-version }}_linux_amd64.tar.gz + run: | + # Downloaded into a directory of its own, since the repository may hold a file or a directory named like the binary. + cd "$(mktemp -d)" + BASE_URL="https://github.com/fossas/fossa-cli/releases/download/v${FOSSA_VERSION}" + + echo "Downloading FOSSA CLI binary and checksum..." + curl -sSL -O "${BASE_URL}/${FOSSA_TARBALL}" + curl -sSL -O "${BASE_URL}/${FOSSA_TARBALL}.sha256" + + echo "Verifying checksum..." + # The checksum file holds the hash, with or without the file name after it. + echo "$(cut -d ' ' -f 1 "${FOSSA_TARBALL}.sha256") ${FOSSA_TARBALL}" | sha256sum --check --status + + if [ $? -eq 0 ]; then + echo "Verification successful!" + tar -xzf "${FOSSA_TARBALL}" fossa + chmod +x fossa + # Installed where the job owns the directory, so neither root nor sudo is needed, on a hosted runner or in a container. + mkdir -p "${RUNNER_TEMP}/bin" + mv fossa "${RUNNER_TEMP}/bin/fossa" + echo "${RUNNER_TEMP}/bin" >> "$GITHUB_PATH" + export PATH="${RUNNER_TEMP}/bin:$PATH" + rm -f "${FOSSA_TARBALL}" "${FOSSA_TARBALL}.sha256" + fossa --version + else + echo "ERROR: Checksum verification failed!" >&2 + exit 1 + fi + + - name: Analyze dependencies + shell: bash + env: + FOSSA_API_KEY: ${{ inputs.api-key }} + run: fossa analyze + + - name: Test dependencies against the policy + if: ${{ inputs.run-tests == 'true' }} + shell: bash + env: + FOSSA_API_KEY: ${{ inputs.api-key }} + DIFF_REVISION: ${{ inputs.test-diff-revision }} + run: | + if [ -n "$DIFF_REVISION" ]; then + fossa test --diff "$DIFF_REVISION" + else + fossa test + fi + + - name: Write HTML report + if: ${{ inputs.report-file != '' }} + shell: bash + env: + FOSSA_API_KEY: ${{ inputs.api-key }} + REPORT_FILE: ${{ inputs.report-file }} + run: | + mkdir -p "$(dirname "$REPORT_FILE")" + fossa report attribution --format html > "$REPORT_FILE" diff --git a/actions/goreleaser/setup/action.yml b/actions/goreleaser/setup/action.yml new file mode 100644 index 0000000..4da2257 --- /dev/null +++ b/actions/goreleaser/setup/action.yml @@ -0,0 +1,47 @@ +name: Setup GoReleaser +description: | + Installs GoReleaser by downloading the official binary from its GitHub release and verifying its SHA256 checksum + (replacement for goreleaser/goreleaser-action, to reduce third-party GitHub Action supply-chain risk). + Linux runners only (for example ubuntu-latest). + +inputs: + goreleaser-version: + description: Version of GoReleaser to install (check latest from https://github.com/goreleaser/goreleaser/releases) + required: false + default: "2.18.2" + +runs: + using: "composite" + steps: + - name: Install GoReleaser + shell: bash + env: + GORELEASER_VERSION: ${{ inputs.goreleaser-version }} + GORELEASER_TARBALL: goreleaser_Linux_x86_64.tar.gz + run: | + # Downloaded into a directory of its own, since the repository may hold a file or a directory named like the binary. + cd "$(mktemp -d)" + BASE_URL="https://github.com/goreleaser/goreleaser/releases/download/v${GORELEASER_VERSION}" + + echo "Downloading GoReleaser binary and checksums..." + curl -sSL -O "${BASE_URL}/${GORELEASER_TARBALL}" + curl -sSL -O "${BASE_URL}/checksums.txt" + + echo "Verifying checksum..." + grep " ${GORELEASER_TARBALL}$" checksums.txt | sha256sum --check --status + + if [ $? -eq 0 ]; then + echo "Verification successful!" + tar -xzf "${GORELEASER_TARBALL}" goreleaser + chmod +x goreleaser + # Installed where the job owns the directory, so neither root nor sudo is needed, on a hosted runner or in a container. + mkdir -p "${RUNNER_TEMP}/bin" + mv goreleaser "${RUNNER_TEMP}/bin/goreleaser" + echo "${RUNNER_TEMP}/bin" >> "$GITHUB_PATH" + export PATH="${RUNNER_TEMP}/bin:$PATH" + rm -f "${GORELEASER_TARBALL}" checksums.txt + goreleaser --version + else + echo "ERROR: Checksum verification failed!" >&2 + exit 1 + fi diff --git a/actions/syft/generate-sbom/action.yml b/actions/syft/generate-sbom/action.yml index c4872ba..b06e833 100644 --- a/actions/syft/generate-sbom/action.yml +++ b/actions/syft/generate-sbom/action.yml @@ -45,6 +45,8 @@ runs: SYFT_TARBALL: syft_${{ inputs.syft-version }}_linux_amd64.tar.gz SYFT_CHECKSUMS: syft_${{ inputs.syft-version }}_checksums.txt run: | + # Downloaded into a directory of its own, since the repository may hold a file or a directory named like the binary. + cd "$(mktemp -d)" BASE_URL="https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}" echo "Downloading Syft binary and checksums..." @@ -58,7 +60,11 @@ runs: echo "Verification successful!" tar -xzf "${SYFT_TARBALL}" syft chmod +x syft - sudo mv syft /usr/local/bin/syft + # Installed where the job owns the directory, so neither root nor sudo is needed, on a hosted runner or in a container. + mkdir -p "${RUNNER_TEMP}/bin" + mv syft "${RUNNER_TEMP}/bin/syft" + echo "${RUNNER_TEMP}/bin" >> "$GITHUB_PATH" + export PATH="${RUNNER_TEMP}/bin:$PATH" rm -f "${SYFT_TARBALL}" "${SYFT_CHECKSUMS}" syft version else diff --git a/actions/terraform/setup/action.yml b/actions/terraform/setup/action.yml index 21d6590..8fd1dab 100644 --- a/actions/terraform/setup/action.yml +++ b/actions/terraform/setup/action.yml @@ -20,6 +20,8 @@ runs: TERRAFORM_ZIP: terraform_${{ inputs.terraform-version }}_linux_amd64.zip TERRAFORM_SHA256SUMS: terraform_${{ inputs.terraform-version }}_SHA256SUMS run: | + # Downloaded into a directory of its own, since the repository may hold a file or a directory named like the binary. + cd "$(mktemp -d)" BASE_URL="https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}" echo "Downloading Terraform binary and checksums..." @@ -33,7 +35,11 @@ runs: echo "Verification successful!" unzip -o "${TERRAFORM_ZIP}" chmod +x terraform - sudo mv terraform /usr/local/bin/terraform + # Installed where the job owns the directory, so neither root nor sudo is needed, on a hosted runner or in a container. + mkdir -p "${RUNNER_TEMP}/bin" + mv terraform "${RUNNER_TEMP}/bin/terraform" + echo "${RUNNER_TEMP}/bin" >> "$GITHUB_PATH" + export PATH="${RUNNER_TEMP}/bin:$PATH" rm -f "${TERRAFORM_ZIP}" "${TERRAFORM_SHA256SUMS}" terraform version else diff --git a/actions/tflint/setup/action.yml b/actions/tflint/setup/action.yml index 770a67b..c60b0aa 100644 --- a/actions/tflint/setup/action.yml +++ b/actions/tflint/setup/action.yml @@ -19,6 +19,8 @@ runs: TFLINT_VERSION: ${{ inputs.tflint-version }} TFLINT_ZIP: tflint_linux_amd64.zip run: | + # Downloaded into a directory of its own, since the repository may hold a file or a directory named like the binary. + cd "$(mktemp -d)" BASE_URL="https://github.com/terraform-linters/tflint/releases/download/${TFLINT_VERSION}" echo "Downloading TFLint binary and checksums..." @@ -32,7 +34,11 @@ runs: echo "Verification successful!" unzip -o "${TFLINT_ZIP}" chmod +x tflint - sudo mv tflint /usr/local/bin/tflint + # Installed where the job owns the directory, so neither root nor sudo is needed, on a hosted runner or in a container. + mkdir -p "${RUNNER_TEMP}/bin" + mv tflint "${RUNNER_TEMP}/bin/tflint" + echo "${RUNNER_TEMP}/bin" >> "$GITHUB_PATH" + export PATH="${RUNNER_TEMP}/bin:$PATH" rm -f "${TFLINT_ZIP}" checksums.txt tflint --version else diff --git a/actions/trivy/scan/action.yml b/actions/trivy/scan/action.yml index 98d7286..fd0eb3d 100644 --- a/actions/trivy/scan/action.yml +++ b/actions/trivy/scan/action.yml @@ -52,6 +52,8 @@ runs: TRIVY_TARBALL: trivy_${{ inputs.trivy-version }}_Linux-64bit.tar.gz TRIVY_CHECKSUMS: trivy_${{ inputs.trivy-version }}_checksums.txt run: | + # Downloaded into a directory of its own, since the repository may hold a file or a directory named like the binary. + cd "$(mktemp -d)" BASE_URL="https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}" echo "Downloading Trivy binary and checksums..." @@ -65,7 +67,11 @@ runs: echo "Verification successful!" tar -xzf "${TRIVY_TARBALL}" trivy chmod +x trivy - sudo mv trivy /usr/local/bin/trivy + # Installed where the job owns the directory, so neither root nor sudo is needed, on a hosted runner or in a container. + mkdir -p "${RUNNER_TEMP}/bin" + mv trivy "${RUNNER_TEMP}/bin/trivy" + echo "${RUNNER_TEMP}/bin" >> "$GITHUB_PATH" + export PATH="${RUNNER_TEMP}/bin:$PATH" rm -f "${TRIVY_TARBALL}" "${TRIVY_CHECKSUMS}" trivy version else