From 771f20f12c6514bc13259103938b5ce909e8088c Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Wed, 23 Sep 2026 12:00:00 +0200 Subject: [PATCH 1/2] ci(release): fix manifest component attribution and harden workflow verification --- .github/workflows/release.yml | 3 + .../validate-release-please-config.yml | 24 ++++ .github/workflows/verify-workflow-sha256.yml | 15 ++- release-please-config.json | 34 +++--- scripts/check-release-please-config.sh | 115 ++++++++++++++++++ scripts/compute-workflow-sha256.sh | 39 +++++- 6 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/validate-release-please-config.yml create mode 100755 scripts/check-release-please-config.sh mode change 100644 => 100755 scripts/compute-workflow-sha256.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4833e83..1038979 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,4 +29,7 @@ jobs: - name: Run release-please uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: main token: ${{ steps.get_token.outputs.token }} diff --git a/.github/workflows/validate-release-please-config.yml b/.github/workflows/validate-release-please-config.yml new file mode 100644 index 0000000..c823d2a --- /dev/null +++ b/.github/workflows/validate-release-please-config.yml @@ -0,0 +1,24 @@ +name: Validate release-please config + +on: + pull_request: + branches: + - main + paths: + - 'release-please-config.json' + - 'scripts/check-release-please-config.sh' + push: + branches: + - main + paths: + - 'release-please-config.json' + - 'scripts/check-release-please-config.sh' + +jobs: + validate: + name: Validate release-please-config.json + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Run config validation + run: bash scripts/check-release-please-config.sh release-please-config.json diff --git a/.github/workflows/verify-workflow-sha256.yml b/.github/workflows/verify-workflow-sha256.yml index c806f1f..06d261f 100644 --- a/.github/workflows/verify-workflow-sha256.yml +++ b/.github/workflows/verify-workflow-sha256.yml @@ -2,12 +2,25 @@ name: verify-workflow-sha256 on: pull_request: + branches: + - main + paths: + - '.github/workflows/*.yml' + - 'scripts/compute-workflow-sha256.sh' + - '**/workflow-sha256' + push: + branches: + - main + paths: + - '.github/workflows/*.yml' + - 'scripts/compute-workflow-sha256.sh' + - '**/workflow-sha256' permissions: contents: read concurrency: - group: verify-workflow-sha256-${{ github.event.pull_request.number }} + group: verify-workflow-sha256-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/release-please-config.json b/release-please-config.json index d701e3b..274a1ec 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,50 +1,44 @@ { "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "separate-pull-requests": true, - "release-type": "simple", - "tag-separator": "-", "always-update": true, - "bump-patch-for-minor-pre-major": true, "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "include-component-in-tag": true, + "include-v-in-tag": true, + "release-type": "simple", + "separate-pull-requests": true, + "tag-separator": "-", "packages": { "conventional-commits": { - "version-file": "conventional-commits/version.txt", - "changelog-path": "conventional-commits/CHANGELOG.md", + "component": "conventional-commits", "extra-label": "conventional-commits" }, "docker-build": { - "version-file": "docker-build/version.txt", - "changelog-path": "docker-build/CHANGELOG.md", + "component": "docker-build", "extra-label": "docker-build" }, "docker-build-cloud": { - "version-file": "docker-build-cloud/version.txt", - "changelog-path": "docker-build-cloud/CHANGELOG.md", + "component": "docker-build-cloud", "extra-label": "docker-build-cloud" }, "propose-safe-multisig-tx": { - "version-file": "propose-safe-multisig-tx/version.txt", - "changelog-path": "propose-safe-multisig-tx/CHANGELOG.md", + "component": "propose-safe-multisig-tx", "extra-label": "propose-safe-multisig-tx" }, "publish-npm": { - "version-file": "publish-npm/version.txt", - "changelog-path": "publish-npm/CHANGELOG.md", + "component": "publish-npm", "extra-label": "publish-npm" }, "release-please": { - "version-file": "release-please/version.txt", - "changelog-path": "release-please/CHANGELOG.md", + "component": "release-please", "extra-label": "release-please" }, "rust-build": { - "version-file": "rust-build/version.txt", - "changelog-path": "rust-build/CHANGELOG.md", + "component": "rust-build", "extra-label": "rust-build" }, "stale": { - "version-file": "stale/version.txt", - "changelog-path": "stale/CHANGELOG.md", + "component": "stale", "extra-label": "stale" } } diff --git a/scripts/check-release-please-config.sh b/scripts/check-release-please-config.sh new file mode 100755 index 0000000..35753c5 --- /dev/null +++ b/scripts/check-release-please-config.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Validates release-please-config.json so manifest releases stay attribution-correct. +# +# The two things that must never regress: +# 1. Every package must declare `component` (or `package-name`); without it +# release-please resolves an empty component, which silently breaks tag +# matching, per-component branch names and per-package pull requests. +# 2. Key ordering ($schema, then lexicographic, packages last) for deterministic diffs. +# +# Usage (works locally or in CI): +# bash scripts/check-release-please-config.sh [path/to/release-please-config.json] +# +# Depends on jq; errors are accumulated and reported all at once. + +set -uo pipefail + +CONFIG="${1:-release-please-config.json}" + +say_err() { echo "error: $*" >&2; } +say_warn() { echo "warning: $*" >&2; } + +failures=0 + +if ! command -v jq >/dev/null 2>&1; then + say_err "jq is required to run this check" + exit 1 +fi + +if [[ ! -f "$CONFIG" ]]; then + say_err "config file not found: $CONFIG" + exit 1 +fi + +# --- JSON validity and object shape --- +if ! jq -e 'type == "object"' "$CONFIG" >/dev/null 2>&1; then + say_err "$CONFIG is not a valid JSON object" + exit 1 +fi + +# --- root key ordering: $schema first, lexicographic, packages last --- +mapfile -t root_keys < <(jq -r 'keys_unsorted[]' "$CONFIG") + +if [[ "${root_keys[0]-}" != '$schema' ]]; then + say_err "root key order: first key must be \"\$schema\" (got \"${root_keys[0]-}\")" + ((failures += 1)) +fi + +if [[ "${root_keys[-1]-}" != "packages" ]]; then + say_err "root key order: \"packages\" must be the last key (got \"${root_keys[-1]-}\")" + ((failures += 1)) +fi + +mid_keys=() +for k in "${root_keys[@]}"; do + [[ "$k" == '$schema' || "$k" == 'packages' ]] && continue + mid_keys+=("$k") +done +mapfile -t mid_sorted < <(printf '%s\n' "${mid_keys[@]}" | sort) +if [[ "${mid_keys[*]:-}" != "${mid_sorted[*]:-}" ]]; then + say_err "root key order: keys after \"\$schema\" must be lexicographic (got: ${mid_keys[*]:-none})" + ((failures += 1)) +fi + +# --- packages: identity + per-package ordering --- +if ! jq -e '.packages | type == "object"' "$CONFIG" >/dev/null 2>&1; then + say_err "key \"packages\" must be an object" + ((failures += 1)) +else + while IFS= read -r pkg; do + if ! jq -e --arg p "$pkg" '.packages[$p] | type == "object" and (has("component") or has("package-name"))' "$CONFIG" >/dev/null 2>&1; then + say_err "packages.$pkg must define \"component\" (or \"package-name\"): required for tag matching, per-component branches and per-package pull requests" + ((failures += 1)) + fi + + mapfile -t pkg_keys < <(jq -r --arg p "$pkg" '.packages[$p] | keys_unsorted[]' "$CONFIG") + mapfile -t pkg_sorted < <(printf '%s\n' "${pkg_keys[@]}" | sort) + if [[ "${pkg_keys[*]:-}" != "${pkg_sorted[*]:-}" ]]; then + say_err "packages.$pkg: keys must be lexicographic (got: ${pkg_keys[*]:-none})" + ((failures += 1)) + fi + + # version-file / changelog-path resolve relative to the package dir; a value + # repeating the dir name produces a doubled path (e.g. docker-build/docker-build/CHANGELOG.md) + for field in version-file changelog-path; do + if jq -e --arg p "$pkg" --arg f "$field" '.packages[$p] | has($f)' "$CONFIG" >/dev/null 2>&1; then + value=$(jq -r --arg p "$pkg" --arg f "$field" '.packages[$p][$f]' "$CONFIG") + case "$field" in + version-file) default_name="version.txt" ;; + changelog-path) default_name="CHANGELOG.md" ;; + esac + if [[ "$value" == *"$(basename "$pkg")"* ]]; then + say_warn "packages.$pkg.$field=\"$value\" is resolved relative to the package dir; use a bare filename (or omit it to use the default /$default_name)" + fi + fi + done + done < <(jq -r '.packages | keys[]' "$CONFIG") +fi + +# --- tag format: must resolve to -vX.Y.Z tags --- +# Existing releases use e.g. docker-build-v2.5.2 / docker-build-cloud-v1.1.1; any +# deviation silently orphans tag matching and walks all history in the next run. +tag_include_component=$(jq -r 'if has("include-component-in-tag") then ."include-component-in-tag" else true end' "$CONFIG") +tag_include_v=$(jq -r 'if has("include-v-in-tag") then ."include-v-in-tag" else true end' "$CONFIG") +tag_separator=$(jq -r 'if has("tag-separator") then ."tag-separator" else "-" end' "$CONFIG") +if [[ "$tag_include_component" != "true" || "$tag_include_v" != "true" || "$tag_separator" != "-" ]]; then + say_err "tags must resolve to -vX.Y.Z: keep include-component-in-tag=true, include-v-in-tag=true and tag-separator=\"-\" (effective: component=$tag_include_component, v-prefix=$tag_include_v, separator=\"$tag_separator\")" + ((failures += 1)) +fi + +if ((failures > 0)); then + echo "release-please-config validation FAILED ($failures error(s))" >&2 + exit 1 +fi + +echo "release-please-config validation OK" diff --git a/scripts/compute-workflow-sha256.sh b/scripts/compute-workflow-sha256.sh old mode 100644 new mode 100755 index 50b0340..59e1488 --- a/scripts/compute-workflow-sha256.sh +++ b/scripts/compute-workflow-sha256.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -euo pipefail +set -uo pipefail cd "$(dirname "$0")/.." @@ -9,13 +9,37 @@ if [[ "${1:-}" == "--check" ]]; then fi outdated=false +errors=0 + +say_err() { echo "error: $*" >&2; } + +if ! command -v jq >/dev/null 2>&1; then + say_err "jq is required to run this script" + exit 1 +fi + +if command -v sha256sum >/dev/null 2>&1; then + sha_cmd=(sha256sum) +elif command -v shasum >/dev/null 2>&1; then + sha_cmd=(shasum -a 256) +else + say_err "neither sha256sum nor shasum is available" + exit 1 +fi mapfile -t package_names < <(jq -r '.packages | keys[]' release-please-config.json) for name in "${package_names[@]}"; do workflow_file=".github/workflows/$name.yml" sha_file="$name/workflow-sha256" - computed_sha="$(sha256sum "$workflow_file" | awk '{print $1}')" + + if [[ ! -f "$workflow_file" ]]; then + say_err "workflow file missing: $workflow_file" + ((errors += 1)) + continue + fi + + computed_sha="$("${sha_cmd[@]}" "$workflow_file" | awk '{print $1}')" if [[ "$check" == true ]]; then if [[ ! -f "$sha_file" ]] || [[ "$(cat "$sha_file")" != "$computed_sha" ]]; then @@ -23,7 +47,11 @@ for name in "${package_names[@]}"; do outdated=true fi else - mkdir -p "$name" + if ! mkdir -p "$name"; then + say_err "could not create package dir: $name" + ((errors += 1)) + continue + fi echo "$computed_sha" > "$sha_file" echo "$sha_file: $computed_sha" >&2 fi @@ -33,3 +61,8 @@ if [[ "$check" == true ]] && [[ "$outdated" == true ]]; then echo "found outdated workflow sha256: execute \`compute-workflow-sha256.sh\` to refresh workflows sha256" exit 1 fi + +if ((errors > 0)); then + say_err "$errors error(s)" + exit 1 +fi From 94d0ac127842f4507452c04434f1301aa8abc565 Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Wed, 23 Sep 2026 14:58:53 +0200 Subject: [PATCH 2/2] ci(release): align validation workflows with repo conventions --- .github/workflows/validate-release-please-config.yml | 11 ++++++++++- .github/workflows/verify-workflow-sha256.yml | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-release-please-config.yml b/.github/workflows/validate-release-please-config.yml index c823d2a..dede66f 100644 --- a/.github/workflows/validate-release-please-config.yml +++ b/.github/workflows/validate-release-please-config.yml @@ -7,18 +7,27 @@ on: paths: - 'release-please-config.json' - 'scripts/check-release-please-config.sh' + - '.github/workflows/validate-release-please-config.yml' push: branches: - main paths: - 'release-please-config.json' - 'scripts/check-release-please-config.sh' + - '.github/workflows/validate-release-please-config.yml' + +permissions: + contents: read + +concurrency: + group: validate-release-please-config-${{ github.ref }} + cancel-in-progress: true jobs: validate: name: Validate release-please-config.json runs-on: ubuntu-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Run config validation run: bash scripts/check-release-please-config.sh release-please-config.json diff --git a/.github/workflows/verify-workflow-sha256.yml b/.github/workflows/verify-workflow-sha256.yml index 06d261f..d929740 100644 --- a/.github/workflows/verify-workflow-sha256.yml +++ b/.github/workflows/verify-workflow-sha256.yml @@ -6,6 +6,7 @@ on: - main paths: - '.github/workflows/*.yml' + - 'release-please-config.json' - 'scripts/compute-workflow-sha256.sh' - '**/workflow-sha256' push: @@ -13,6 +14,7 @@ on: - main paths: - '.github/workflows/*.yml' + - 'release-please-config.json' - 'scripts/compute-workflow-sha256.sh' - '**/workflow-sha256'