mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26074f9390 | ||
|
|
0ad2de90ee | ||
|
|
af04e69dc7 | ||
|
|
935316cb51 |
238
.github/scripts/upload-to-r2.js
vendored
238
.github/scripts/upload-to-r2.js
vendored
@@ -5,6 +5,8 @@ const {
|
|||||||
} = require("@aws-sdk/client-s3");
|
} = require("@aws-sdk/client-s3");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const https = require("https");
|
||||||
|
const { pipeline } = require("stream/promises");
|
||||||
|
|
||||||
const s3Client = new S3Client({
|
const s3Client = new S3Client({
|
||||||
region: "auto",
|
region: "auto",
|
||||||
@@ -18,6 +20,7 @@ const s3Client = new S3Client({
|
|||||||
const BUCKET = process.env.R2_BUCKET_NAME;
|
const BUCKET = process.env.R2_BUCKET_NAME;
|
||||||
const PUBLIC_URL = process.env.R2_PUBLIC_URL;
|
const PUBLIC_URL = process.env.R2_PUBLIC_URL;
|
||||||
const VERSION = process.env.RELEASE_VERSION;
|
const VERSION = process.env.RELEASE_VERSION;
|
||||||
|
const RELEASE_TAG = process.env.RELEASE_TAG || `v${VERSION}`;
|
||||||
const GITHUB_REPO = process.env.GITHUB_REPOSITORY;
|
const GITHUB_REPO = process.env.GITHUB_REPOSITORY;
|
||||||
|
|
||||||
async function fetchExistingReleases() {
|
async function fetchExistingReleases() {
|
||||||
@@ -62,8 +65,230 @@ function findArtifacts(dir, pattern) {
|
|||||||
return files.filter((f) => pattern.test(f)).map((f) => path.join(dir, f));
|
return files.filter((f) => pattern.test(f)).map((f) => path.join(dir, f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
const request = https.get(url, { timeout: 10000 }, (response) => {
|
||||||
|
const statusCode = response.statusCode;
|
||||||
|
|
||||||
|
// Follow redirects
|
||||||
|
if (
|
||||||
|
statusCode === 302 ||
|
||||||
|
statusCode === 301 ||
|
||||||
|
statusCode === 307 ||
|
||||||
|
statusCode === 308
|
||||||
|
) {
|
||||||
|
const redirectUrl = response.headers.location;
|
||||||
|
response.destroy();
|
||||||
|
if (!redirectUrl) {
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode,
|
||||||
|
error: "Redirect without location header",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Follow the redirect URL
|
||||||
|
return https
|
||||||
|
.get(redirectUrl, { timeout: 10000 }, (redirectResponse) => {
|
||||||
|
const redirectStatus = redirectResponse.statusCode;
|
||||||
|
const contentType =
|
||||||
|
redirectResponse.headers["content-type"] || "";
|
||||||
|
// Check if it's actually a file (zip/tar.gz) and not HTML
|
||||||
|
const isFile =
|
||||||
|
contentType.includes("application/zip") ||
|
||||||
|
contentType.includes("application/gzip") ||
|
||||||
|
contentType.includes("application/x-gzip") ||
|
||||||
|
contentType.includes("application/x-tar") ||
|
||||||
|
redirectUrl.includes(".zip") ||
|
||||||
|
redirectUrl.includes(".tar.gz");
|
||||||
|
const isGood =
|
||||||
|
redirectStatus >= 200 && redirectStatus < 300 && isFile;
|
||||||
|
redirectResponse.destroy();
|
||||||
|
resolve({
|
||||||
|
accessible: isGood,
|
||||||
|
statusCode: redirectStatus,
|
||||||
|
finalUrl: redirectUrl,
|
||||||
|
contentType,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("error", (error) => {
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("timeout", function () {
|
||||||
|
this.destroy();
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode,
|
||||||
|
error: "Timeout following redirect",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if status is good (200-299 range) and it's actually a file
|
||||||
|
const contentType = response.headers["content-type"] || "";
|
||||||
|
const isFile =
|
||||||
|
contentType.includes("application/zip") ||
|
||||||
|
contentType.includes("application/gzip") ||
|
||||||
|
contentType.includes("application/x-gzip") ||
|
||||||
|
contentType.includes("application/x-tar") ||
|
||||||
|
url.includes(".zip") ||
|
||||||
|
url.includes(".tar.gz");
|
||||||
|
const isGood = statusCode >= 200 && statusCode < 300 && isFile;
|
||||||
|
response.destroy();
|
||||||
|
resolve({ accessible: isGood, statusCode, contentType });
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", (error) => {
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode: null,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("timeout", () => {
|
||||||
|
request.destroy();
|
||||||
|
resolve({
|
||||||
|
accessible: false,
|
||||||
|
statusCode: null,
|
||||||
|
error: "Request timeout",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.accessible) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
console.log(
|
||||||
|
`✓ URL ${url} is now accessible after ${attempt} retries (status: ${result.statusCode})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`✓ URL ${url} is accessible (status: ${result.statusCode})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result.finalUrl || url; // Return the final URL (after redirects) if available
|
||||||
|
} else {
|
||||||
|
const errorMsg = result.error ? ` - ${result.error}` : "";
|
||||||
|
const statusMsg = result.statusCode
|
||||||
|
? ` (status: ${result.statusCode})`
|
||||||
|
: "";
|
||||||
|
const contentTypeMsg = result.contentType
|
||||||
|
? ` [content-type: ${result.contentType}]`
|
||||||
|
: "";
|
||||||
|
console.log(
|
||||||
|
`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`✗ URL ${url} check failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < maxRetries - 1) {
|
||||||
|
const delay = initialDelay * Math.pow(2, attempt);
|
||||||
|
console.log(
|
||||||
|
` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`URL ${url} is not accessible after ${maxRetries} attempts`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFromGitHub(url, outputPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = https.get(url, { timeout: 30000 }, (response) => {
|
||||||
|
const statusCode = response.statusCode;
|
||||||
|
|
||||||
|
// Follow redirects (all redirect types)
|
||||||
|
if (
|
||||||
|
statusCode === 301 ||
|
||||||
|
statusCode === 302 ||
|
||||||
|
statusCode === 307 ||
|
||||||
|
statusCode === 308
|
||||||
|
) {
|
||||||
|
const redirectUrl = response.headers.location;
|
||||||
|
response.destroy();
|
||||||
|
if (!redirectUrl) {
|
||||||
|
reject(new Error(`Redirect without location header for ${url}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Resolve relative redirects
|
||||||
|
const finalRedirectUrl = redirectUrl.startsWith("http")
|
||||||
|
? redirectUrl
|
||||||
|
: new URL(redirectUrl, url).href;
|
||||||
|
console.log(` Following redirect: ${finalRedirectUrl}`);
|
||||||
|
return downloadFromGitHub(finalRedirectUrl, outputPath)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode !== 200) {
|
||||||
|
response.destroy();
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Failed to download ${url}: ${statusCode} ${response.statusMessage}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStream = fs.createWriteStream(outputPath);
|
||||||
|
response.pipe(fileStream);
|
||||||
|
fileStream.on("finish", () => {
|
||||||
|
fileStream.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
fileStream.on("error", (error) => {
|
||||||
|
response.destroy();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", reject);
|
||||||
|
request.on("timeout", () => {
|
||||||
|
request.destroy();
|
||||||
|
reject(new Error(`Request timeout for ${url}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const artifactsDir = "artifacts";
|
const artifactsDir = "artifacts";
|
||||||
|
const tempDir = path.join(artifactsDir, "temp");
|
||||||
|
|
||||||
|
// Create temp directory for downloaded GitHub archives
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download source archives from GitHub
|
||||||
|
const githubZipUrl = `https://github.com/${GITHUB_REPO}/archive/refs/tags/${RELEASE_TAG}.zip`;
|
||||||
|
const githubTarGzUrl = `https://github.com/${GITHUB_REPO}/archive/refs/tags/${RELEASE_TAG}.tar.gz`;
|
||||||
|
|
||||||
|
const sourceZipPath = path.join(tempDir, `automaker-${VERSION}.zip`);
|
||||||
|
const sourceTarGzPath = path.join(tempDir, `automaker-${VERSION}.tar.gz`);
|
||||||
|
|
||||||
|
console.log(`Waiting for source archives to be available on GitHub...`);
|
||||||
|
console.log(` ZIP: ${githubZipUrl}`);
|
||||||
|
console.log(` TAR.GZ: ${githubTarGzUrl}`);
|
||||||
|
|
||||||
|
// Wait for archives to be accessible with exponential backoff
|
||||||
|
// This returns the final URL after following redirects
|
||||||
|
const finalZipUrl = await checkUrlAccessible(githubZipUrl);
|
||||||
|
const finalTarGzUrl = await checkUrlAccessible(githubTarGzUrl);
|
||||||
|
|
||||||
|
console.log(`Downloading source archives from GitHub...`);
|
||||||
|
await downloadFromGitHub(finalZipUrl, sourceZipPath);
|
||||||
|
await downloadFromGitHub(finalTarGzUrl, sourceTarGzPath);
|
||||||
|
|
||||||
|
console.log(`Downloaded source archives successfully`);
|
||||||
|
|
||||||
// Find all artifacts
|
// Find all artifacts
|
||||||
const artifacts = {
|
const artifacts = {
|
||||||
@@ -77,6 +302,8 @@ async function main() {
|
|||||||
path.join(artifactsDir, "linux-builds"),
|
path.join(artifactsDir, "linux-builds"),
|
||||||
/\.AppImage$/
|
/\.AppImage$/
|
||||||
),
|
),
|
||||||
|
sourceZip: [sourceZipPath],
|
||||||
|
sourceTarGz: [sourceTarGzPath],
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Found artifacts:");
|
console.log("Found artifacts:");
|
||||||
@@ -97,6 +324,8 @@ async function main() {
|
|||||||
macos: "application/x-apple-diskimage",
|
macos: "application/x-apple-diskimage",
|
||||||
macosArm: "application/x-apple-diskimage",
|
macosArm: "application/x-apple-diskimage",
|
||||||
linux: "application/x-executable",
|
linux: "application/x-executable",
|
||||||
|
sourceZip: "application/zip",
|
||||||
|
sourceTarGz: "application/gzip",
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [platform, files] of Object.entries(artifacts)) {
|
for (const [platform, files] of Object.entries(artifacts)) {
|
||||||
@@ -115,7 +344,12 @@ async function main() {
|
|||||||
url: `${PUBLIC_URL}/releases/${VERSION}/${filename}`,
|
url: `${PUBLIC_URL}/releases/${VERSION}/${filename}`,
|
||||||
filename,
|
filename,
|
||||||
size,
|
size,
|
||||||
arch: platform === "macosArm" ? "arm64" : "x64",
|
arch:
|
||||||
|
platform === "macosArm"
|
||||||
|
? "arm64"
|
||||||
|
: platform === "sourceZip" || platform === "sourceTarGz"
|
||||||
|
? "source"
|
||||||
|
: "x64",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +360,7 @@ async function main() {
|
|||||||
version: VERSION,
|
version: VERSION,
|
||||||
date: new Date().toISOString(),
|
date: new Date().toISOString(),
|
||||||
assets,
|
assets,
|
||||||
githubReleaseUrl: `https://github.com/${GITHUB_REPO}/releases/tag/${VERSION}`,
|
githubReleaseUrl: `https://github.com/${GITHUB_REPO}/releases/tag/${RELEASE_TAG}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove existing entry for this version if re-running
|
// Remove existing entry for this version if re-running
|
||||||
|
|||||||
29
.github/workflows/release.yml
vendored
29
.github/workflows/release.yml
vendored
@@ -48,6 +48,21 @@ jobs:
|
|||||||
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
|
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- name: Extract and set version
|
||||||
|
id: version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}"
|
||||||
|
# Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
|
||||||
|
VERSION="${VERSION_TAG#v}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Extracted version: $VERSION from tag: $VERSION_TAG"
|
||||||
|
# Update the app's package.json version
|
||||||
|
cd apps/app
|
||||||
|
npm version $VERSION --no-git-tag-version
|
||||||
|
cd ../..
|
||||||
|
echo "Updated apps/app/package.json to version $VERSION"
|
||||||
|
|
||||||
- name: Build Electron App (macOS)
|
- name: Build Electron App (macOS)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
env:
|
env:
|
||||||
@@ -127,6 +142,17 @@ jobs:
|
|||||||
- name: Install AWS SDK
|
- name: Install AWS SDK
|
||||||
run: npm install @aws-sdk/client-s3
|
run: npm install @aws-sdk/client-s3
|
||||||
|
|
||||||
|
- name: Extract version
|
||||||
|
id: version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION_TAG="${{ github.event.inputs.version || github.ref_name }}"
|
||||||
|
# Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
|
||||||
|
VERSION="${VERSION_TAG#v}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "version_tag=$VERSION_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Extracted version: $VERSION from tag: $VERSION_TAG"
|
||||||
|
|
||||||
- name: Upload to R2 and update releases.json
|
- name: Upload to R2 and update releases.json
|
||||||
env:
|
env:
|
||||||
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
@@ -134,6 +160,7 @@ jobs:
|
|||||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
|
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
|
||||||
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
|
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
|
||||||
RELEASE_VERSION: ${{ github.event.inputs.version || github.ref_name }}
|
RELEASE_VERSION: ${{ steps.version.outputs.version }}
|
||||||
|
RELEASE_TAG: ${{ steps.version.outputs.version_tag }}
|
||||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
run: node .github/scripts/upload-to-r2.js
|
run: node .github/scripts/upload-to-r2.js
|
||||||
|
|||||||
Reference in New Issue
Block a user