Skip to content

Commit a66877f

Browse files
committed
feat: add reusable workflow to check PR labels for release conditions
Add check-pr-labels.yml workflow that checks for labels on PRs (or merged PRs on push events) and outputs boolean values for common release labels: - github-release - crates-release - prerelease - npm-release Supports both pull_request and push events, with optional default values for direct pushes
1 parent 0a619a5 commit a66877f

12 files changed

Lines changed: 562 additions & 345 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: 'Create GitHub Release'
2+
description: 'Create a GitHub release with artifacts'
3+
4+
inputs:
5+
version:
6+
description: 'The version to release (e.g., 1.2.3)'
7+
required: true
8+
title:
9+
description: 'The release title'
10+
required: true
11+
notes:
12+
description: 'Release notes in markdown'
13+
required: false
14+
default: ''
15+
prerelease:
16+
description: 'Whether this is a prerelease'
17+
required: false
18+
default: false
19+
draft:
20+
description: 'Whether to create as draft'
21+
required: false
22+
default: false
23+
tag-prefix:
24+
description: 'Prefix for the git tag'
25+
required: false
26+
default: 'v'
27+
artifact-pattern:
28+
description: 'Pattern to match artifacts to attach (leave empty for none)'
29+
required: false
30+
default: ''
31+
32+
outputs:
33+
url:
34+
description: 'URL of the created release'
35+
value: ${{ steps.gh-release.outputs.url }}
36+
tag:
37+
description: 'The created tag name'
38+
value: ${{ inputs.tag-prefix }}${{ inputs.version }}
39+
40+
runs:
41+
using: 'composite'
42+
steps:
43+
- name: Download artifacts
44+
if: inputs.artifact-pattern != ''
45+
uses: actions/download-artifact@v4
46+
with:
47+
path: artifacts
48+
pattern: ${{ inputs.artifact-pattern }}
49+
merge-multiple: true
50+
51+
- name: Create Release
52+
id: gh-release
53+
uses: softprops/action-gh-release@v2
54+
with:
55+
tag_name: ${{ inputs.tag-prefix }}${{ inputs.version }}
56+
name: ${{ inputs.title }}
57+
body: |
58+
${{ inputs.notes }}
59+
60+
---
61+
**Version:** ${{ inputs.version }}
62+
draft: ${{ inputs.draft }}
63+
prerelease: ${{ inputs.prerelease }}
64+
files: ${{ inputs.artifact-pattern != '' && 'artifacts/*' || '' }}
65+
env:
66+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
67+
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
name: 'Generate Release Info'
2+
description: 'Generate version number and release notes using AI'
3+
4+
inputs:
5+
service-name:
6+
description: 'The name of the service (used in release notes)'
7+
required: true
8+
prerelease:
9+
description: 'Whether this is a prerelease (appends -rc.N suffix)'
10+
required: false
11+
default: false
12+
13+
outputs:
14+
version:
15+
description: 'The determined version (e.g., 1.2.3 or 1.2.3-rc.5)'
16+
value: ${{ steps.version.outputs.final }}
17+
release_notes:
18+
description: 'AI-generated release notes in markdown'
19+
value: ${{ steps.ai-notes.outputs.final-message }}
20+
21+
runs:
22+
using: 'composite'
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v5
26+
with:
27+
fetch-depth: 0
28+
29+
- name: Get last release info
30+
id: last-release
31+
uses: actions/github-script@v7
32+
with:
33+
script: |
34+
try {
35+
const { data: releases } = await github.rest.repos.listReleases({
36+
owner: context.repo.owner,
37+
repo: context.repo.repo,
38+
per_page: 1
39+
});
40+
41+
if (releases.length > 0) {
42+
const tagName = releases[0].tag_name;
43+
const { data: ref } = await github.rest.git.getRef({
44+
owner: context.repo.owner,
45+
repo: context.repo.repo,
46+
ref: `tags/${tagName}`
47+
});
48+
core.setOutput('sha', ref.object.sha);
49+
core.setOutput('version', tagName.replace(/^v/, ''));
50+
} else {
51+
const allCommits = await github.paginate(github.rest.repos.listCommits, {
52+
owner: context.repo.owner,
53+
repo: context.repo.repo,
54+
per_page: 100
55+
});
56+
core.setOutput('sha', allCommits[allCommits.length - 1].sha);
57+
core.setOutput('version', '0.0.0');
58+
}
59+
} catch (error) {
60+
core.setOutput('sha', context.sha);
61+
core.setOutput('version', '0.0.0');
62+
}
63+
64+
- name: AI Determine Version
65+
id: ai-version
66+
uses: openai/codex-action@v1
67+
env:
68+
OPENAI_API_KEY: ${{ env.OPENAI_API_KEY }}
69+
with:
70+
openai-api-key: ${{ env.OPENAI_API_KEY }}
71+
safety-strategy: read-only
72+
prompt: |
73+
Analyze commits between ${{ steps.last-release.outputs.sha }} and ${{ github.sha }}.
74+
Current version: ${{ steps.last-release.outputs.version }}
75+
Prerelease: ${{ inputs.prerelease }}
76+
77+
Rules:
78+
- BREAKING CHANGE or ! = major bump
79+
- feat: = minor bump
80+
- fix:, perf:, or other = patch bump
81+
- Default to patch if unclear
82+
- For prereleases: use same version logic, suffix will be added automatically
83+
84+
Respond with ONLY X.Y.Z
85+
86+
- name: Parse Version
87+
id: version
88+
shell: bash
89+
run: |
90+
NEXT=$(echo "${{ steps.ai-version.outputs.final-message }}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
91+
[ -z "$NEXT" ] && { echo "::error::Invalid version"; exit 1; }
92+
93+
if [ "${{ inputs.prerelease }}" == "true" ]; then
94+
echo "final=${NEXT}-rc.${GITHUB_RUN_NUMBER}" >> $GITHUB_OUTPUT
95+
else
96+
echo "final=${NEXT}" >> $GITHUB_OUTPUT
97+
fi
98+
99+
- name: AI Generate Notes
100+
id: ai-notes
101+
uses: openai/codex-action@v1
102+
with:
103+
prompt: |
104+
Generate release notes for ${{ inputs.service-name }} v${{ steps.version.outputs.final }}.
105+
Changes from ${{ steps.last-release.outputs.sha }} to ${{ github.sha }}.
106+
107+
REQUIREMENTS:
108+
1. NO title - start with content directly
109+
2. Plain language - like explaining to a friend
110+
3. SHORT - 1-2 sentences per item
111+
4. Group: ## New Features, ## Bug Fixes, ## Improvement, if they exist. For example,
112+
if there are no new features, bug fixes, or improvements, don't include them.
113+
5. Only include breaking changes if they exist
114+
6. Include commit SHAs if necessary. If the commit is large, don't include it.
115+
7. 1-2 emojis max for personality
116+
8. Code diffs ONLY if necessary. If the diff is large, don't include it.
117+
118+
The end goal is clear business technical writing that is easy to understand, use, and bring
119+
delight to the user
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: 'Publish to Crates.io'
2+
description: 'Publish a Rust crate to crates.io'
3+
4+
inputs:
5+
crate-name:
6+
description: 'The name of the crate to publish (from Cargo.toml)'
7+
required: true
8+
crate-path:
9+
description: 'Optional: Path to crate directory (e.g., crates/client). If empty, publishes from workspace root using -p flag.'
10+
required: false
11+
default: ''
12+
dry-run:
13+
description: 'Run cargo publish --dry-run instead of actual publish'
14+
required: false
15+
default: false
16+
17+
runs:
18+
using: 'composite'
19+
steps:
20+
- name: Setup Rust
21+
uses: dtolnay/rust-toolchain@stable
22+
23+
- name: Publish to crates.io
24+
shell: bash
25+
env:
26+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
27+
run: |
28+
if [ -n "${{ inputs.crate-path }}" ]; then
29+
# Publish from crate directory
30+
echo "Publishing ${{ inputs.crate-name }} from ${{ inputs.crate-path }}..."
31+
cd "${{ inputs.crate-path }}"
32+
if [ "${{ inputs.dry-run }}" == "true" ]; then
33+
cargo publish --dry-run
34+
else
35+
cargo publish
36+
fi
37+
else
38+
# Publish from workspace root using -p flag
39+
echo "Publishing ${{ inputs.crate-name }} from workspace root..."
40+
if [ "${{ inputs.dry-run }}" == "true" ]; then
41+
cargo publish --dry-run -p "${{ inputs.crate-name }}"
42+
else
43+
cargo publish -p "${{ inputs.crate-name }}"
44+
fi
45+
fi
46+

.github/workflows/publish-npm.yml renamed to .github/workflows/blocks/actions/publish-npm/action.yaml

File renamed without changes.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: 'Rust Build'
2+
description: 'Build Rust binary for multiple targets'
3+
4+
inputs:
5+
binary-name:
6+
description: 'The name of the crate/binary to build (from Cargo.toml)'
7+
required: true
8+
binary-path:
9+
description: 'Optional: Path to crate directory (e.g., "crates/client"). If empty, builds from workspace root using -p flag.'
10+
required: false
11+
default: ''
12+
target:
13+
description: 'Target triple (e.g., x86_64-unknown-linux-gnu)'
14+
required: true
15+
build-env:
16+
description: 'Compile-time env vars for .env file (e.g., "BASE_URL=https://example.com")'
17+
required: false
18+
default: ''
19+
20+
outputs:
21+
artifact-name:
22+
description: 'Name of the artifact'
23+
value: ${{ steps.package.outputs.artifact-name }}
24+
artifact-path:
25+
description: 'Path to the built artifact'
26+
value: ${{ steps.package.outputs.artifact-path }}
27+
28+
runs:
29+
using: 'composite'
30+
steps:
31+
- name: Setup Rust
32+
uses: dtolnay/rust-toolchain@stable
33+
with:
34+
targets: ${{ inputs.target }}
35+
36+
- name: Install deps (Linux)
37+
if: runner.os == 'Linux'
38+
shell: bash
39+
run: sudo apt-get update && sudo apt-get install -y libssl-dev pkg-config
40+
41+
- name: Install deps (macOS)
42+
if: runner.os == 'macOS'
43+
shell: bash
44+
run: brew install openssl@3
45+
46+
- name: Set OpenSSL env (macOS)
47+
if: runner.os == 'macOS'
48+
shell: bash
49+
run: |
50+
OPENSSL_DIR=$(brew --prefix openssl@3)
51+
echo "OPENSSL_DIR=${OPENSSL_DIR}" >> $GITHUB_ENV
52+
echo "PKG_CONFIG_PATH=${OPENSSL_DIR}/lib/pkgconfig" >> $GITHUB_ENV
53+
54+
- name: Create .env file
55+
if: inputs.build-env != ''
56+
shell: bash
57+
run: echo "${{ inputs.build-env }}" > .env
58+
59+
- name: Build
60+
shell: bash
61+
run: |
62+
if [ -n "${{ inputs.binary-path }}" ]; then
63+
# Build from crate directory
64+
cd "${{ inputs.binary-path }}"
65+
cargo build --release --target ${{ inputs.target }}
66+
else
67+
# Build from workspace root using -p flag
68+
cargo build --release --target ${{ inputs.target }} -p ${{ inputs.binary-name }}
69+
fi
70+
71+
- name: Package
72+
id: package
73+
shell: bash
74+
run: |
75+
mkdir -p artifacts
76+
if [ "${{ runner.os }}" == "Windows" ]; then
77+
ARTIFACT_NAME="${{ inputs.binary-name }}-${{ inputs.target }}.exe"
78+
ARTIFACT_PATH="artifacts/$ARTIFACT_NAME"
79+
cp target/${{ inputs.target }}/release/${{ inputs.binary-name }}.exe "$ARTIFACT_PATH"
80+
else
81+
ARTIFACT_NAME="${{ inputs.binary-name }}-${{ inputs.target }}"
82+
ARTIFACT_PATH="artifacts/$ARTIFACT_NAME"
83+
cp target/${{ inputs.target }}/release/${{ inputs.binary-name }} "$ARTIFACT_PATH"
84+
chmod +x "$ARTIFACT_PATH"
85+
fi
86+
echo "artifact-name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
87+
echo "artifact-path=$ARTIFACT_PATH" >> $GITHUB_OUTPUT
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: 'TypeScript Build'
2+
description: 'Build TypeScript project using Bun'
3+
4+
inputs:
5+
bun-version:
6+
description: 'Bun version to use'
7+
required: false
8+
default: 'latest'
9+
working-directory:
10+
description: 'Directory containing package.json'
11+
required: false
12+
default: '.'
13+
build-command:
14+
description: 'Build command to run'
15+
required: false
16+
default: 'bun run build'
17+
18+
runs:
19+
using: 'composite'
20+
steps:
21+
- name: Setup Bun
22+
uses: oven-sh/setup-bun@v2
23+
with:
24+
bun-version: ${{ inputs.bun-version }}
25+
26+
- name: Install dependencies
27+
shell: bash
28+
working-directory: ${{ inputs.working-directory }}
29+
run: bun install
30+
31+
- name: Build
32+
shell: bash
33+
working-directory: ${{ inputs.working-directory }}
34+
run: ${{ inputs.build-command }}

0 commit comments

Comments
 (0)