Skip to content
This repository was archived by the owner on Jul 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
90 changes: 87 additions & 3 deletions scripts/release-check
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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() {
Expand Down
65 changes: 65 additions & 0 deletions scripts/test-release-check
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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

Expand Down
Loading