Skip to content
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
98 changes: 98 additions & 0 deletions .github/workflows/release-on-resolved-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Release a tested, resolved product issue

on:
workflow_run:
workflows: ["Tests"]
types: [completed]

permissions:
actions: write
contents: write
issues: read
pull-requests: read

concurrency:
group: cli-release
cancel-in-progress: false

jobs:
tag:
name: Tag a reviewed, integrated product release
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
persist-credentials: false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- id: release
name: Validate the tested resolving PR and calculate a tag
env:
GH_TOKEN: ${{ github.token }}
TESTED_SHA: ${{ github.event.workflow_run.head_sha }}
shell: bash
run: |
set -euo pipefail
query='query($owner:String!, $repo:String!) { repository(owner:$owner, name:$repo) { pullRequests(first:100, states:MERGED, baseRefName:"main", orderBy:{field:UPDATED_AT,direction:DESC}) { nodes { number mergeCommit { oid } labels(first:20) { nodes { name } } closingIssuesReferences(first:20) { nodes { number repository { nameWithOwner } labels(first:30) { nodes { name } } } } } } } }'
result="$(gh api graphql -f query="$query" -f owner="${GITHUB_REPOSITORY_OWNER}" -f repo="${GITHUB_REPOSITORY#*/}")"
matching="$(jq --arg sha "$TESTED_SHA" '[.data.repository.pullRequests.nodes[] | select(.mergeCommit.oid == $sha)]' <<< "$result")"
count="$(jq 'length' <<< "$matching")"
[ "$count" -eq 1 ] || { echo "Expected exactly one merged PR for tested SHA $TESTED_SHA; found $count." >&2; exit 1; }

labels="$(jq -r '.[0].labels.nodes[].name' <<< "$matching")"
has_label() { grep -Fxq "$1" <<< "$labels"; }
has_label 'release:publish' || { echo 'Resolving PR must carry release:publish.' >&2; exit 1; }
mapfile -t bump_labels < <(grep -E '^release:(patch|minor|major)$' <<< "$labels" || true)
[ "${#bump_labels[@]}" -eq 1 ] || { echo 'Resolving PR must carry exactly one release:patch, release:minor, or release:major label.' >&2; exit 1; }
bump="${bump_labels[0]#release:}"

issues="$(jq --arg repo "$GITHUB_REPOSITORY" '[.[0].closingIssuesReferences.nodes[] | select(.repository.nameWithOwner == $repo)]' <<< "$matching")"
issue_count="$(jq 'length' <<< "$issues")"
[ "$issue_count" -eq 1 ] || { echo "Expected exactly one same-repository issue closed by the PR; found $issue_count." >&2; exit 1; }
issue_labels="$(jq -r '.[0].labels.nodes[].name' <<< "$issues")"
issue_has_label() { grep -Fxq "$1" <<< "$issue_labels"; }
issue_has_label 'release:ready' || { echo 'Closed issue must carry release:ready before merge.' >&2; exit 1; }
issue_has_label 'direction:aligned' || { echo 'Closed issue must carry direction:aligned.' >&2; exit 1; }
issue_has_label 'roadmap' || { echo 'Closed issue must carry roadmap.' >&2; exit 1; }
! issue_has_label 'direction:revise' || { echo 'Issue is still direction:revise.' >&2; exit 1; }
! issue_has_label 'direction:discuss-close' || { echo 'Issue is direction:discuss-close.' >&2; exit 1; }

latest="$(git tag -l 'cli-v*' --sort=-v:refname | head -n1 || true)"
if [[ "$latest" =~ ^cli-v([0-9]+)\.([0-9]+)\.([0-9]+)-[0-9a-f]{12}$ ]]; then
base="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
else
legacy="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)"
[[ "$legacy" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] || { echo 'No valid immutable or legacy CLI release tag was found.' >&2; exit 1; }
base="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
fi
IFS='.' read -r major minor patch <<< "$base"
case "$bump" in
major) version="$((major + 1)).0.0" ;;
minor) version="$major.$((minor + 1)).0" ;;
patch) version="$major.$minor.$((patch + 1))" ;;
esac
grep -Eq "version=[\"']${version}[\"']" setup.py || { echo "setup.py must declare ${version}." >&2; exit 1; }
grep -Eq "__version__[[:space:]]*=[[:space:]]*[\"']${version}[\"']" diffgraph/__init__.py || { echo "diffgraph/__init__.py must declare ${version}." >&2; exit 1; }
tag="cli-v${version}-${TESTED_SHA:0:12}"
git rev-parse -q --verify "refs/tags/${tag}" >/dev/null && { echo "Release tag already exists: ${tag}" >&2; exit 1; }
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Create immutable release tag at the tested commit
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.release.outputs.tag }}
TESTED_SHA: ${{ github.event.workflow_run.head_sha }}
shell: bash
run: |
set -euo pipefail
git tag -a "$TAG" "$TESTED_SHA" -m "Release $TAG"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git push origin "$TAG"
- name: Publish the immutable tag
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.release.outputs.tag }}
run: gh workflow run release.yml --repo "$GITHUB_REPOSITORY" --ref main -f release_tag="$TAG"
73 changes: 65 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,60 @@ name: Release native binaries
on:
push:
tags:
- "v*"
- "cli-v*"
workflow_dispatch:
inputs:
release_tag:
description: "Immutable CLI tag to build and publish"
required: true
type: string

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
group: release-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: false

env:
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}

jobs:
validate:
name: Validate immutable release tag
runs-on: ubuntu-latest
outputs:
source_sha: ${{ steps.source.outputs.sha }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_TAG }}
fetch-depth: 0
persist-credentials: false
- id: source
shell: bash
run: |
set -euo pipefail
tag="${RELEASE_TAG}"
if [[ "$tag" =~ ^cli-v([0-9]+)\.([0-9]+)\.([0-9]+)-([0-9a-f]{12})$ ]]; then
version="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
encoded_sha="${BASH_REMATCH[4]}"
else
echo "Expected cli-v<semver>-<12-char-sha>; got $tag" >&2
exit 1
fi
grep -Eq "version=[\"']${version}[\"']" setup.py || { echo "setup.py must declare ${version}." >&2; exit 1; }
grep -Eq "__version__[[:space:]]*=[[:space:]]*[\"']${version}[\"']" diffgraph/__init__.py || { echo "diffgraph/__init__.py must declare ${version}." >&2; exit 1; }
sha="$(git rev-parse HEAD)"
if [[ "${sha:0:12}" != "$encoded_sha" ]]; then
echo "Tag $tag must encode the checked-out commit; got $sha" >&2
exit 1
fi
echo "sha=$sha" >> "$GITHUB_OUTPUT"

build:
name: Build ${{ matrix.target }}
permissions:
contents: read
needs: validate
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -46,6 +86,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.source_sha }}
persist-credentials: false
- uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -78,15 +119,28 @@ jobs:

publish:
name: Verify assets and create release
needs: build
needs: [validate, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- name: Verify immutable tag still resolves to the validated source
env:
SOURCE_SHA: ${{ needs.validate.outputs.source_sha }}
shell: bash
run: |
set -euo pipefail
git fetch --no-tags origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
tag_sha="$(git rev-parse "${RELEASE_TAG}^{commit}")"
test "$tag_sha" = "$SOURCE_SHA" || {
echo "Tag ${RELEASE_TAG} resolves to $tag_sha, not validated source $SOURCE_SHA." >&2
exit 1
}
- name: Download all native binaries
uses: actions/download-artifact@v4
with:
Expand Down Expand Up @@ -135,17 +189,20 @@ jobs:
];
fs.writeFileSync('release/cli-manifest.json', JSON.stringify({
schemaVersion: 1,
version: process.env.GITHUB_REF_NAME,
version: process.env.RELEASE_TAG,
sourceCommit: process.env.SOURCE_SHA,
assets,
}, null, 2) + '\n');
NODE
env:
SOURCE_SHA: ${{ needs.validate.outputs.source_sha }}
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
gh release create "${GITHUB_REF_NAME}" release/* \
gh release create "${RELEASE_TAG}" release/* \
--repo "${GITHUB_REPOSITORY}" \
--title "${GITHUB_REF_NAME}" \
--title "${RELEASE_TAG}" \
--generate-notes
Loading