Skip to content

Commit 82b8dd5

Browse files
authored
Merge pull request #1 from MacPaw/chore/cicd
chore: ci/cd
2 parents 08b118a + 950931b commit 82b8dd5

File tree

9 files changed

+442
-8
lines changed

9 files changed

+442
-8
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Build Multi-Platform Binary
2+
3+
permissions:
4+
contents: write
5+
6+
on:
7+
push:
8+
tags:
9+
- '*'
10+
11+
jobs:
12+
build-binary:
13+
strategy:
14+
matrix:
15+
include:
16+
- os: macos-latest
17+
platform: macOS
18+
arch: universal
19+
- os: macos-latest
20+
platform: macOS
21+
arch: arm64
22+
- os: macos-latest
23+
platform: macOS
24+
arch: x86_64
25+
- os: ubuntu-latest
26+
platform: linux
27+
arch: x86_64
28+
runs-on: ${{ matrix.os }}
29+
30+
steps:
31+
- name: Checkout repository
32+
uses: actions/checkout@v4
33+
with:
34+
fetch-depth: 0
35+
36+
- name: Get latest tag and checkout
37+
id: get-tag
38+
run: ./.github/workflows/scripts/build-multi-platform-binary/get-latest-tag.sh
39+
40+
- name: Check or create release
41+
id: check-release
42+
env:
43+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44+
TAG_NAME: ${{ steps.get-tag.outputs.tag_name }}
45+
run: node ./.github/workflows/scripts/build-multi-platform-binary/check-release.js
46+
47+
- name: Check if asset already exists
48+
id: check-asset
49+
env:
50+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51+
RELEASE_ID: ${{ steps.check-release.outputs.release_id }}
52+
TAG_NAME: ${{ steps.get-tag.outputs.tag_name }}
53+
PLATFORM: ${{ matrix.platform }}
54+
ARCH: ${{ matrix.arch }}
55+
run: node ./.github/workflows/scripts/build-multi-platform-binary/check-asset.js
56+
57+
- name: Build binary
58+
id: build
59+
if: steps.check-asset.outputs.asset_exists == 'false'
60+
env:
61+
TAG_NAME: ${{ steps.get-tag.outputs.tag_name }}
62+
PLATFORM: ${{ matrix.platform }}
63+
ARCH: ${{ matrix.arch }}
64+
run: ./.github/workflows/scripts/build-multi-platform-binary/build-binary.sh
65+
66+
- name: Upload asset to release
67+
if: steps.build.conclusion == 'success'
68+
env:
69+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70+
RELEASE_ID: ${{ steps.check-release.outputs.release_id }}
71+
ASSET_PATH: ${{ steps.build.outputs.asset_path }}
72+
ASSET_NAME: ${{ steps.build.outputs.asset_name }}
73+
run: node ./.github/workflows/scripts/build-multi-platform-binary/upload-asset.js
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Extract version from tag (remove 'v' prefix if present)
6+
VERSION=${TAG_NAME#v}
7+
BINARY_NAME="BinaryDependenciesManager"
8+
PLATFORM=${PLATFORM:-macOS}
9+
ARCH=${ARCH:-universal}
10+
ARCHIVE_NAME="BinaryDependenciesManager-${PLATFORM}-${ARCH}-${VERSION}.zip"
11+
12+
echo "Building binary for platform: $PLATFORM, architecture: $ARCH, version: $VERSION"
13+
14+
# Create build directory
15+
mkdir -p build
16+
17+
if [ "$PLATFORM" == "macOS" ] && [ "$ARCH" == "universal" ]; then
18+
# Build universal binary for macOS
19+
echo "Building universal binary for macOS..."
20+
swift build --configuration release --arch x86_64 --arch arm64
21+
cp .build/apple/Products/Release/$BINARY_NAME build/$BINARY_NAME
22+
23+
elif [ "$PLATFORM" == "macOS" ] && [ "$ARCH" == "arm64" || "$ARCH" == "x86_64" ]; then
24+
25+
# Build $ARCH binary for macOS
26+
echo "Building $ARCH binary for macOS..."
27+
swift build --configuration release --arch $ARCH
28+
cp .build/$ARCH-apple-macosx/release/$BINARY_NAME build/$BINARY_NAME
29+
30+
# Verify the binary
31+
echo "Verifying binary..."
32+
file build/$BINARY_NAME
33+
elif [ "$PLATFORM" == "linux" ] && [ "$ARCH" == "x86_64" ]; then
34+
# Build for Linux x86_64
35+
echo "Building for Linux x86_64..."
36+
swift build --configuration release
37+
cp .build/release/$BINARY_NAME build/$BINARY_NAME
38+
39+
# Verify the binary
40+
echo "Verifying binary..."
41+
file build/$BINARY_NAME
42+
else
43+
echo "Unsupported platform/architecture combination: $PLATFORM/$ARCH"
44+
exit 1
45+
fi
46+
47+
# Create the archive
48+
echo "Creating archive: $ARCHIVE_NAME"
49+
cd build
50+
51+
if [ "$PLATFORM" == "macOS" ]; then
52+
# Use ditto on macOS
53+
ditto -c -k --sequesterRsrc --keepParent $BINARY_NAME "../$ARCHIVE_NAME"
54+
elif [ "$PLATFORM" == "linux" ]; then
55+
# Use zip on Linux
56+
zip "../$ARCHIVE_NAME" $BINARY_NAME
57+
else
58+
echo "Unsupported platform for archiving: $PLATFORM"
59+
exit 1
60+
fi
61+
62+
cd ..
63+
64+
# Verify the archive was created
65+
if [ -f "$ARCHIVE_NAME" ]; then
66+
echo "Archive created successfully: $ARCHIVE_NAME"
67+
echo "Archive size: $(du -h $ARCHIVE_NAME | cut -f1)"
68+
69+
# Set outputs for GitHub Actions
70+
echo "asset_path=$PWD/$ARCHIVE_NAME" >> $GITHUB_OUTPUT
71+
echo "asset_name=$ARCHIVE_NAME" >> $GITHUB_OUTPUT
72+
else
73+
echo "Error: Archive was not created"
74+
exit 1
75+
fi
76+
77+
# Clean up build artifacts
78+
rm -rf build
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const { execSync } = require('child_process');
2+
const fs = require('fs');
3+
4+
async function checkAsset() {
5+
const releaseId = process.env.RELEASE_ID;
6+
const tagName = process.env.TAG_NAME;
7+
const token = process.env.GITHUB_TOKEN;
8+
const platform = process.env.PLATFORM;
9+
const arch = process.env.ARCH;
10+
11+
if (!releaseId || !tagName || !token || !platform || !arch) {
12+
console.error('RELEASE_ID, TAG_NAME, PLATFORM, ARCH, and GITHUB_TOKEN environment variables are required');
13+
process.exit(1);
14+
}
15+
16+
// Get repository info from git remote
17+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim();
18+
const repoMatch = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
19+
20+
if (!repoMatch) {
21+
console.error('Could not parse GitHub repository from remote URL');
22+
process.exit(1);
23+
}
24+
25+
const [, owner, repo] = repoMatch;
26+
27+
// Extract version from tag (remove 'v' prefix if present)
28+
const version = tagName.replace(/^v/, '');
29+
const expectedAssetName = `BinaryDependenciesManager-${version}-${platform}-${arch}.zip`;
30+
31+
console.log(`Checking for asset: ${expectedAssetName} in release ${releaseId}`);
32+
33+
try {
34+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/${releaseId}/assets`, {
35+
headers: {
36+
'Authorization': `token ${token}`,
37+
'Accept': 'application/vnd.github.v3+json',
38+
'User-Agent': 'GitHub-Actions'
39+
}
40+
});
41+
42+
if (response.status === 200) {
43+
const assets = await response.json();
44+
const existingAsset = assets.find(asset => asset.name === expectedAssetName);
45+
46+
if (existingAsset) {
47+
console.log(`Asset already exists: ${existingAsset.name} (ID: ${existingAsset.id})`);
48+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `asset_exists=true\n`);
49+
} else {
50+
console.log('Asset does not exist, proceeding with build');
51+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `asset_exists=false\n`);
52+
}
53+
} else {
54+
console.error(`API request failed with status: ${response.status}`);
55+
const errorText = await response.text();
56+
console.error(`Response: ${errorText}`);
57+
process.exit(1);
58+
}
59+
} catch (error) {
60+
console.error('Error checking asset:', error.message);
61+
process.exit(1);
62+
}
63+
}
64+
65+
checkAsset();
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
const { execSync } = require('child_process');
2+
const fs = require('fs');
3+
4+
async function generateReleaseNotes(owner, repo, tagName, token) {
5+
console.log(`Generating release notes for tag: ${tagName}`);
6+
7+
try {
8+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/generate-notes`, {
9+
method: 'POST',
10+
headers: {
11+
'Authorization': `token ${token}`,
12+
'Accept': 'application/vnd.github.v3+json',
13+
'User-Agent': 'GitHub-Actions',
14+
'Content-Type': 'application/json'
15+
},
16+
body: JSON.stringify({
17+
tag_name: tagName
18+
})
19+
});
20+
21+
if (response.status === 200) {
22+
const releaseNotes = await response.json();
23+
return releaseNotes.body;
24+
} else {
25+
console.warn(`Failed to generate release notes: ${response.status}`);
26+
return `Release ${tagName}`;
27+
}
28+
} catch (error) {
29+
console.warn('Error generating release notes:', error.message);
30+
return `Release ${tagName}`;
31+
}
32+
}
33+
34+
async function createRelease(owner, repo, tagName, token) {
35+
console.log(`Creating release for tag: ${tagName}`);
36+
37+
// Generate release notes
38+
const releaseBody = await generateReleaseNotes(owner, repo, tagName, token);
39+
40+
try {
41+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`, {
42+
method: 'POST',
43+
headers: {
44+
'Authorization': `token ${token}`,
45+
'Accept': 'application/vnd.github.v3+json',
46+
'User-Agent': 'GitHub-Actions',
47+
'Content-Type': 'application/json'
48+
},
49+
body: JSON.stringify({
50+
tag_name: tagName,
51+
name: `Release ${tagName}`,
52+
body: releaseBody,
53+
draft: false,
54+
prerelease: false,
55+
generate_release_notes: false // We already generated them manually
56+
})
57+
});
58+
59+
if (response.status === 201) {
60+
const release = await response.json();
61+
console.log(`Release created successfully: ${release.name} (ID: ${release.id})`);
62+
return release;
63+
} else {
64+
const errorText = await response.text();
65+
console.error(`Failed to create release: ${response.status}`);
66+
console.error(`Response: ${errorText}`);
67+
process.exit(1);
68+
}
69+
} catch (error) {
70+
console.error('Error creating release:', error.message);
71+
process.exit(1);
72+
}
73+
}
74+
75+
async function checkOrCreateRelease() {
76+
const tagName = process.env.TAG_NAME;
77+
const token = process.env.GITHUB_TOKEN;
78+
79+
if (!tagName || !token) {
80+
console.error('TAG_NAME and GITHUB_TOKEN environment variables are required');
81+
process.exit(1);
82+
}
83+
84+
// Get repository info from git remote
85+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim();
86+
const repoMatch = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
87+
88+
if (!repoMatch) {
89+
console.error('Could not parse GitHub repository from remote URL');
90+
process.exit(1);
91+
}
92+
93+
const [, owner, repo] = repoMatch;
94+
95+
console.log(`Checking release for tag: ${tagName} in ${owner}/${repo}`);
96+
97+
try {
98+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/tags/${tagName}`, {
99+
headers: {
100+
'Authorization': `token ${token}`,
101+
'Accept': 'application/vnd.github.v3+json',
102+
'User-Agent': 'GitHub-Actions'
103+
}
104+
});
105+
106+
if (response.status === 200) {
107+
const release = await response.json();
108+
console.log(`Release exists: ${release.name} (ID: ${release.id})`);
109+
110+
// Set outputs for GitHub Actions
111+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `release_exists=true\n`);
112+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `release_id=${release.id}\n`);
113+
} else if (response.status === 404) {
114+
console.log('Release does not exist, creating new release...');
115+
116+
// Create a new release
117+
const newRelease = await createRelease(owner, repo, tagName, token);
118+
119+
// Set outputs for GitHub Actions
120+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `release_exists=true\n`);
121+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `release_id=${newRelease.id}\n`);
122+
} else {
123+
console.error(`API request failed with status: ${response.status}`);
124+
const errorText = await response.text();
125+
console.error(`Response: ${errorText}`);
126+
process.exit(1);
127+
}
128+
} catch (error) {
129+
console.error('Error checking/creating release:', error.message);
130+
process.exit(1);
131+
}
132+
}
133+
134+
checkOrCreateRelease();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Get the latest tag
6+
LATEST_TAG=$(git describe --tags --abbrev=0)
7+
8+
echo "Latest tag: $LATEST_TAG"
9+
10+
# Checkout the latest tag
11+
git checkout "$LATEST_TAG"
12+
13+
# Set output for GitHub Actions
14+
echo "tag_name=$LATEST_TAG" >> $GITHUB_OUTPUT
15+
16+
echo "Checked out tag: $LATEST_TAG"

0 commit comments

Comments
 (0)