-
Notifications
You must be signed in to change notification settings - Fork 3
ci: Add mirror release guardrails #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jbeckwith-oai
merged 5 commits into
main
from
codex/code-scan-action-release-guardrails
Apr 13, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c214d10
Add release PR validation and tag automation
jbeckwith-oai 19d72cf
Create GitHub releases for mirrored action tags
jbeckwith-oai 0a285fa
Address mirror release workflow review feedback
jbeckwith-oai be1c5ce
Recheck main tip before writing release tags
jbeckwith-oai 825e516
Use release metadata to detect stale tag runs
jbeckwith-oai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| name: Move release tags | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - .release-source.json | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| concurrency: | ||
| group: release-tags-main | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| move-release-tags: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Skip non-release commits | ||
| id: release-metadata | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| if [[ ! -f .release-source.json ]]; then | ||
| echo "release_commit=false" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| version="$(jq -r '.version' .release-source.json)" | ||
| if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "Invalid release version in .release-source.json: $version" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "release_commit=true" >> "$GITHUB_OUTPUT" | ||
| echo "version=$version" >> "$GITHUB_OUTPUT" | ||
| echo "source_sha=$(jq -r '.sourceSha' .release-source.json)" >> "$GITHUB_OUTPUT" | ||
| echo "major=${version%%.*}" >> "$GITHUB_OUTPUT" | ||
| echo "minor=${version%.*}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Update floating and exact version tags | ||
| if: ${{ steps.release-metadata.outputs.release_commit == 'true' }} | ||
| env: | ||
| VERSION: ${{ steps.release-metadata.outputs.version }} | ||
| SOURCE_SHA: ${{ steps.release-metadata.outputs.source_sha }} | ||
| MAJOR: ${{ steps.release-metadata.outputs.major }} | ||
| MINOR: ${{ steps.release-metadata.outputs.minor }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| git tag -fa "v${VERSION}" -m "Release v${VERSION}" | ||
| git tag -fa "v${MINOR}" -m "Release v${MINOR}" | ||
| git tag -fa "v${MAJOR}" -m "Release v${MAJOR}" | ||
|
|
||
| git fetch origin main | ||
| main_version="$(git show origin/main:.release-source.json | jq -r '.version')" | ||
| main_source_sha="$(git show origin/main:.release-source.json | jq -r '.sourceSha')" | ||
| if [[ "$VERSION" != "$main_version" || "$SOURCE_SHA" != "$main_source_sha" ]]; then | ||
| echo "Skipping stale tag update for release $VERSION; origin/main now points to $main_version from $main_source_sha" | ||
| exit 0 | ||
| fi | ||
|
|
||
| git push --force origin "refs/tags/v${VERSION}" "refs/tags/v${MINOR}" "refs/tags/v${MAJOR}" | ||
|
|
||
| - name: Create or update GitHub release | ||
| if: ${{ steps.release-metadata.outputs.release_commit == 'true' }} | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| VERSION: ${{ steps.release-metadata.outputs.version }} | ||
| SOURCE_SHA: ${{ steps.release-metadata.outputs.source_sha }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| notes_file="$RUNNER_TEMP/release-notes.md" | ||
| awk -v version="$VERSION" ' | ||
| $0 ~ "^## \\[" version "\\]" { | ||
| in_section=1 | ||
| next | ||
| } | ||
| in_section && /^## \[/ { | ||
| exit | ||
| } | ||
| in_section { | ||
| } | ||
| ' CHANGELOG.md > "$notes_file" | ||
|
|
||
| if [[ ! -s "$notes_file" ]]; then | ||
| printf 'Release v%s\n' "$VERSION" > "$notes_file" | ||
| fi | ||
|
|
||
| git fetch origin main | ||
| main_version="$(git show origin/main:.release-source.json | jq -r '.version')" | ||
| main_source_sha="$(git show origin/main:.release-source.json | jq -r '.sourceSha')" | ||
| if [[ "$VERSION" != "$main_version" || "$SOURCE_SHA" != "$main_source_sha" ]]; then | ||
| echo "Skipping stale release update for release $VERSION; origin/main now points to $main_version from $main_source_sha" | ||
| exit 0 | ||
| fi | ||
|
|
||
| if gh release view "v${VERSION}" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then | ||
| gh release edit "v${VERSION}" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --title "v${VERSION}" \ | ||
| --notes-file "$notes_file" \ | ||
| --target "$GITHUB_SHA" \ | ||
| --verify-tag | ||
| else | ||
| gh release create "v${VERSION}" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --title "v${VERSION}" \ | ||
| --notes-file "$notes_file" \ | ||
| --target "$GITHUB_SHA" \ | ||
| --verify-tag | ||
| fi | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| name: Validate release PR | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: | ||
| - main | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| validate-release-pr: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Validate maintenance PR scope | ||
| if: ${{ !startsWith(github.head_ref, 'release/code-scan-action-v') }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| while IFS= read -r file; do | ||
| [[ -z "$file" ]] && continue | ||
| if [[ ! "$file" =~ ^\.github/ ]]; then | ||
| echo "Maintenance PRs may only change .github/* files. Unexpected file: $file" | ||
| exit 1 | ||
| fi | ||
| done < <(git diff --name-only origin/main...HEAD) | ||
|
|
||
| echo "Branch ${{ github.head_ref }} is not a generated release branch; artifact mirror files are unchanged." | ||
|
|
||
| - name: Validate generated release payload | ||
| if: ${{ startsWith(github.head_ref, 'release/code-scan-action-v') }} | ||
| env: | ||
| HEAD_REF: ${{ github.head_ref }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| if [[ ! -f .release-source.json ]]; then | ||
| echo "Missing .release-source.json in generated release PR" | ||
| exit 1 | ||
| fi | ||
|
|
||
| jq -e ' | ||
| .repository == "promptfoo/promptfoo" | ||
| and .packagePath == "code-scan-action" | ||
| and (.sourceSha | test("^[0-9a-f]{40}$")) | ||
| and (.sourceTag | type == "string" and length > 0) | ||
| and (.version | test("^[0-9]+\\.[0-9]+\\.[0-9]+$")) | ||
| ' .release-source.json > /dev/null | ||
|
|
||
| version="$(jq -r '.version' .release-source.json)" | ||
| source_sha="$(jq -r '.sourceSha' .release-source.json)" | ||
| source_tag="$(jq -r '.sourceTag' .release-source.json)" | ||
|
|
||
| if [[ "$HEAD_REF" != "release/code-scan-action-v${version}" ]]; then | ||
| echo "Branch name $HEAD_REF does not match release version $version" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [[ "$source_tag" != "code-scan-action-${version}" ]]; then | ||
| echo "Source tag $source_tag does not match release version $version" | ||
| exit 1 | ||
| fi | ||
|
|
||
| while IFS= read -r file; do | ||
| [[ -z "$file" ]] && continue | ||
| if [[ ! "$file" =~ ^(action\.yml|README\.md|CHANGELOG\.md|\.release-source\.json|dist(/.*)?)$ ]]; then | ||
| echo "Unexpected generated release file: $file" | ||
| exit 1 | ||
| fi | ||
| done < <(git diff --name-only origin/main...HEAD) | ||
|
|
||
| source_dir="$RUNNER_TEMP/promptfoo-source" | ||
| expected_dir="$RUNNER_TEMP/code-scan-action-expected" | ||
|
|
||
| rm -rf "$source_dir" "$expected_dir" | ||
| git clone https://github.com/promptfoo/promptfoo.git "$source_dir" | ||
|
|
||
| cd "$source_dir" | ||
| git checkout --detach "$source_sha" | ||
| git merge-base --is-ancestor "$source_sha" origin/main | ||
|
|
||
| resolved_source_tag_sha="$(git rev-parse "${source_tag}^{commit}")" | ||
| if [[ "$resolved_source_tag_sha" != "$source_sha" ]]; then | ||
| echo "Source tag $source_tag points to $resolved_source_tag_sha, expected $source_sha" | ||
| exit 1 | ||
| fi | ||
|
|
||
| npm install -g npm@latest | ||
| npm ci | ||
| npm ci --prefix code-scan-action | ||
| npm run tsc --prefix code-scan-action | ||
| npm run build --prefix code-scan-action | ||
|
|
||
| mkdir -p "$expected_dir" | ||
| cp code-scan-action/action.yml "$expected_dir/action.yml" | ||
| cp code-scan-action/README.md "$expected_dir/README.md" | ||
| cp code-scan-action/CHANGELOG.md "$expected_dir/CHANGELOG.md" | ||
| cp -R code-scan-action/dist "$expected_dir/dist" | ||
| cp "$GITHUB_WORKSPACE/.release-source.json" "$expected_dir/.release-source.json" | ||
|
|
||
| for file in action.yml README.md CHANGELOG.md .release-source.json; do | ||
| if ! diff -u "$expected_dir/$file" "$GITHUB_WORKSPACE/$file"; then | ||
| echo "Generated mirror file $file does not match promptfoo/promptfoo@$source_sha" | ||
| exit 1 | ||
| fi | ||
| done | ||
|
|
||
| if ! diff -qr "$expected_dir/dist" "$GITHUB_WORKSPACE/dist"; then | ||
| echo "Generated mirror dist/ does not match promptfoo/promptfoo@$source_sha" | ||
| exit 1 | ||
| fi |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.