diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 073bb66..3c00eaf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,9 +21,6 @@ jobs: with: fetch-depth: 0 - - name: Validate release tag - run: ./scripts/release-check release "$GITHUB_REF_NAME" - - name: Require annotated tag run: | test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = tag @@ -35,6 +32,9 @@ jobs: tag_commit="$(git rev-parse "refs/tags/$GITHUB_REF_NAME^{commit}")" git merge-base --is-ancestor "$tag_commit" refs/remotes/origin/main + - name: Validate release tag + run: ./scripts/release-check release "$GITHUB_REF_NAME" + - name: Install container tooling run: | sudo apt-get update diff --git a/CHANGELOG.md b/CHANGELOG.md index ad87fbc..a58cf7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- GitHub Release publication now establishes annotated-tag and main-ancestry + trust before running repository release code from the tagged checkout. - `RUNA_REF` tag checkout now resolves SemVer-shaped values only through explicit tag refs so homonymous branches cannot shadow release inputs. - Release tag validation now rejects leading-zero numeric identifiers so base diff --git a/RELEASING.md b/RELEASING.md index bcfe380..0b4423d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -79,10 +79,11 @@ corrected by cutting the next `rc.N`, not by rewriting the existing tag. ## Post-Release Gate The tag push runs `.github/workflows/release.yml`. That workflow verifies the -annotated tag, builds a local container image with `BASE_REF` set to the tag, -verifies image identity, extracts release notes from `CHANGELOG.md`, and -publishes the GitHub Release. Only `vX.Y.Z-rc.N` tags are published as GitHub -prereleases. +annotated tag and main-branch ancestry with git-only checks before running +repository release code, builds a local container image with `BASE_REF` set to +the tag, verifies image identity, extracts release notes from `CHANGELOG.md`, +and publishes the GitHub Release. Only `vX.Y.Z-rc.N` tags are published as +GitHub prereleases. Manual GitHub Release creation, when needed after a workflow failure, uses the same notes source and release classification. diff --git a/scripts/release-check b/scripts/release-check index 13fdb03..88cd597 100755 --- a/scripts/release-check +++ b/scripts/release-check @@ -147,6 +147,83 @@ workflow_tag_patterns() { ' "$workflow" } +workflow_executable_lines() { + local workflow="$1" + + awk ' + function indent_of(line, i, char) { + for (i = 1; i <= length(line); i++) { + char = substr(line, i, 1) + if (char != " ") { + return i - 1 + } + } + return length(line) + } + + function trim(value) { + sub(/^[[:space:]]+/, "", value) + sub(/[[:space:]]+$/, "", value) + return value + } + + function unquote(value) { + if ((value ~ /^".*"$/) || (value ~ /^\047.*\047$/)) { + return substr(value, 2, length(value) - 2) + } + return value + } + + function strip_shell_comment(value) { + sub(/(^|[[:space:]])#.*/, "", value) + return value + } + + function emit(line_number, command) { + command = strip_shell_comment(command) + command = trim(command) + if (command == "" || command ~ /^#/) { + return + } + print line_number "\t" command + } + + { + line = $0 + sub(/\r$/, "", line) + indent = indent_of(line) + stripped = trim(line) + + if (in_run_block) { + if (stripped != "" && indent <= run_indent) { + in_run_block = 0 + } else { + emit(NR, line) + next + } + } + + if (stripped ~ /^#/) { + next + } + + if (line ~ /^[[:space:]]*(-[[:space:]]*)?run:[[:space:]]*/) { + value = line + sub(/^[[:space:]]*(-[[:space:]]*)?run:[[:space:]]*/, "", value) + value = trim(value) + + if (value ~ /^[|>]/) { + in_run_block = 1 + run_indent = indent + next + } + + emit(NR, unquote(value)) + } + } + ' "$workflow" +} + check_release_workflow_surface() { local workflow="$repo_root/.github/workflows/release.yml" [[ -f "$workflow" ]] || die ".github/workflows/release.yml not found" @@ -158,11 +235,18 @@ check_release_workflow_surface() { ! grep -Eq '^ paths:' "$workflow" \ || die ".github/workflows/release.yml tag publication must not use paths filters" - local validation_line container_setup_line - validation_line="$(awk '/^[[:space:]]*(-[[:space:]]*)?run:[[:space:]]+\.\/scripts\/release-check release "\$GITHUB_REF_NAME"[[:space:]]*$/ { print NR; exit }' "$workflow")" - container_setup_line="$(awk '/sudo apt-get install -y podman|podman build/ { print NR; exit }' "$workflow")" + local validation_line container_setup_line annotated_tag_line main_ancestry_line repository_code_line + validation_line="$(workflow_executable_lines "$workflow" | awk -F '\t' 'index($2, "./scripts/release-check release \"$GITHUB_REF_NAME\"") { print $1; exit }')" + container_setup_line="$(workflow_executable_lines "$workflow" | awk -F '\t' '$2 ~ /sudo apt-get install -y podman|podman build/ { print $1; exit }')" [[ -n "$validation_line" ]] && [[ -n "$container_setup_line" ]] && [[ "$validation_line" -lt "$container_setup_line" ]] \ || die ".github/workflows/release.yml must validate the release tag before container setup" + + annotated_tag_line="$(workflow_executable_lines "$workflow" | awk -F '\t' '$2 ~ /git cat-file -t/ { print $1; exit }')" + main_ancestry_line="$(workflow_executable_lines "$workflow" | awk -F '\t' '$2 ~ /git merge-base --is-ancestor/ { print $1; exit }')" + repository_code_line="$(workflow_executable_lines "$workflow" | awk -F '\t' 'index($2, "./scripts/") || $2 ~ /podman build/ { print $1; exit }')" + [[ -n "$annotated_tag_line" ]] && [[ -n "$main_ancestry_line" ]] && [[ -n "$repository_code_line" ]] \ + && [[ "$annotated_tag_line" -lt "$repository_code_line" ]] && [[ "$main_ancestry_line" -lt "$repository_code_line" ]] \ + || die ".github/workflows/release.yml must establish tag trust before running repository code" } check_release_metadata_workflow_surface() { diff --git a/scripts/test-release-check b/scripts/test-release-check index 5b2175d..2f6a991 100755 --- a/scripts/test-release-check +++ b/scripts/test-release-check @@ -66,6 +66,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - run: test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = tag + - run: git merge-base --is-ancestor "$tag_commit" refs/remotes/origin/main - run: ./scripts/release-check release "$GITHUB_REF_NAME" - run: sudo apt-get install -y podman - run: podman build . @@ -475,6 +477,63 @@ release_workflow_rejects_late_validation() { assert_failure_contains "${FUNCNAME[0]}" "validate the release tag before container setup" run_check "$root" metadata } +release_workflow_ignores_yaml_comments_when_ordering_validation() { + local root + root="$(new_fixture workflow-yaml-comment-validation 1.2.3)" + sed -i '/release-check release "\$GITHUB_REF_NAME"/d' "$root/.github/workflows/release.yml" + sed -i '/sudo apt-get install -y podman/i\ # Note: validate via ./scripts/release-check release "$GITHUB_REF_NAME"' "$root/.github/workflows/release.yml" + sed -i '/podman build ./a\ - run: ./scripts/release-check release "$GITHUB_REF_NAME"' "$root/.github/workflows/release.yml" + assert_failure_contains "${FUNCNAME[0]}" "validate the release tag before container setup" run_check "$root" metadata +} + +release_workflow_ignores_inline_shell_comments_when_ordering_validation() { + local root + root="$(new_fixture workflow-inline-shell-comment-validation 1.2.3)" + sed -i '/release-check release "\$GITHUB_REF_NAME"/d' "$root/.github/workflows/release.yml" + sed -i '/sudo apt-get install -y podman/i\ - run: true # ./scripts/release-check release "$GITHUB_REF_NAME"' "$root/.github/workflows/release.yml" + sed -i '/podman build ./a\ - run: ./scripts/release-check release "$GITHUB_REF_NAME"' "$root/.github/workflows/release.yml" + assert_failure_contains "${FUNCNAME[0]}" "validate the release tag before container setup" run_check "$root" metadata +} + +release_workflow_rejects_validation_before_tag_trust() { + local root + root="$(new_fixture workflow-pretrust-validation 1.2.3)" + sed -i '/release-check release "\$GITHUB_REF_NAME"/d' "$root/.github/workflows/release.yml" + sed -i '/uses: actions\/checkout@v4/a\ - run: ./scripts/release-check release "$GITHUB_REF_NAME"' "$root/.github/workflows/release.yml" + assert_failure_contains "${FUNCNAME[0]}" "establish tag trust before running repository code" run_check "$root" metadata +} + +release_workflow_ignores_shell_comments_when_establishing_tag_trust() { + local root + root="$(new_fixture workflow-shell-comment-trust 1.2.3)" + sed -i '/git cat-file -t/d' "$root/.github/workflows/release.yml" + sed -i '/uses: actions\/checkout@v4/a\ - run: |\n # git cat-file -t "refs/tags/$GITHUB_REF_NAME"' "$root/.github/workflows/release.yml" + assert_failure_contains "${FUNCNAME[0]}" "establish tag trust before running repository code" run_check "$root" metadata +} + +release_workflow_ignores_inline_shell_comments_when_establishing_tag_trust() { + local root + root="$(new_fixture workflow-inline-shell-comment-trust 1.2.3)" + sed -i '/git cat-file -t/d' "$root/.github/workflows/release.yml" + sed -i '/git merge-base --is-ancestor/d' "$root/.github/workflows/release.yml" + sed -i '/uses: actions\/checkout@v4/a\ - run: true # git merge-base --is-ancestor "$tag_commit" refs/remotes/origin/main' "$root/.github/workflows/release.yml" + sed -i '/uses: actions\/checkout@v4/a\ - run: true # git cat-file -t "refs/tags/$GITHUB_REF_NAME"' "$root/.github/workflows/release.yml" + assert_failure_contains "${FUNCNAME[0]}" "establish tag trust before running repository code" run_check "$root" metadata +} + +release_workflow_establishes_tag_trust_before_repository_code_execution() { + local workflow="$workspace_root/.github/workflows/release.yml" + local annotated_tag_line main_ancestry_line repository_code_line + + annotated_tag_line="$(awk '/git cat-file -t/ { print NR; exit }' "$workflow")" + main_ancestry_line="$(awk '/git merge-base --is-ancestor/ { print NR; exit }' "$workflow")" + repository_code_line="$(awk 'index($0, "./scripts/release-check release \"$GITHUB_REF_NAME\"") || /podman build/ { print NR; exit }' "$workflow")" + + [[ -n "$annotated_tag_line" ]] && [[ -n "$main_ancestry_line" ]] && [[ -n "$repository_code_line" ]] \ + && [[ "$annotated_tag_line" -lt "$repository_code_line" ]] && [[ "$main_ancestry_line" -lt "$repository_code_line" ]] \ + || fail "${FUNCNAME[0]}" +} + release_workflow_uses_semver_rc_classifier() { local workflow="$workspace_root/.github/workflows/release.yml" @@ -515,6 +574,12 @@ release_rejects_container_label_mismatches release_workflow_uses_broad_tag_filter_with_early_validation release_workflow_rejects_missing_early_validation release_workflow_rejects_late_validation +release_workflow_ignores_yaml_comments_when_ordering_validation +release_workflow_ignores_inline_shell_comments_when_ordering_validation +release_workflow_rejects_validation_before_tag_trust +release_workflow_ignores_shell_comments_when_establishing_tag_trust +release_workflow_ignores_inline_shell_comments_when_establishing_tag_trust +release_workflow_establishes_tag_trust_before_repository_code_execution release_workflow_uses_semver_rc_classifier release_validation_rejects_malformed_broad_trigger_tags