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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
Expand All @@ -19,5 +21,14 @@ jobs:

- run: npm ci

- name: Validate release pull request
if: startsWith(github.head_ref, 'release/')
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
GITHUB_HEAD_REF: ${{ github.head_ref }}
run: |
git fetch --quiet origin main
node scripts/check-release-pr.js

- name: Unit tests
run: npx vitest run --exclude='tests/e2e/**'
21 changes: 12 additions & 9 deletions .github/workflows/release-prep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ on:
description: 'Release bullets separated with literal \n sequences'
required: true
type: string
base_ref:
description: 'Base branch or ref to prepare the release from'
required: false
default: 'main'
type: string

permissions:
contents: write

Expand All @@ -30,13 +24,13 @@ jobs:
env:
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_CHANGELOG: ${{ inputs.changelog }}
RELEASE_BASE_REF: ${{ inputs.base_ref }}
RELEASE_BASE_REF: main

steps:
- name: Checkout base ref
- name: Checkout main
uses: actions/checkout@v4
with:
ref: ${{ inputs.base_ref }}
ref: main
fetch-depth: 0

- name: Set up Node
Expand Down Expand Up @@ -86,3 +80,12 @@ jobs:
git add manifest.json package.json package-lock.json data/version-info.json CHANGELOG.md
git commit -m "chore: bump version to ${RELEASE_VERSION}"
git push origin "release/${RELEASE_VERSION}"

- name: Write release pull request summary
run: |
{
echo "## Release branch ready"
echo
echo "Open and merge the release PR before publishing:"
echo "https://github.com/${GITHUB_REPOSITORY}/compare/main...release/${RELEASE_VERSION}?expand=1"
} >> "$GITHUB_STEP_SUMMARY"
148 changes: 148 additions & 0 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: Release Publish

on:
workflow_dispatch:
inputs:
version:
description: 'Version to publish (x.y.z)'
required: true
type: string
pull_request:
description: 'Merged release pull request number'
required: true
type: string

permissions:
contents: read

concurrency:
group: release-publish-${{ inputs.version }}
cancel-in-progress: false

jobs:
validate:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
release_sha: ${{ steps.release_gate.outputs.release_sha }}
env:
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_PR_NUMBER: ${{ inputs.pull_request }}

steps:
- name: Require main workflow ref
run: |
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
echo "Release Publish must be dispatched from main"
exit 1
fi

- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Validate merged release pull request
id: release_gate
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/validate-release-publish.js

- name: Run unit tests
run: npx vitest run --exclude='tests/e2e/**'

- name: Build release packages
run: npm run release:package

- name: Create release notes
run: node scripts/write-release-notes.js

- name: Upload verified release assets
uses: actions/upload-artifact@v4
with:
name: panelize-${{ inputs.version }}-publish
path: |
dist/release-${{ inputs.version }}/panelize-${{ inputs.version }}-release.zip
dist/release-${{ inputs.version }}/panelize-${{ inputs.version }}-cws.zip
dist/release-${{ inputs.version }}/release-notes.md
if-no-files-found: error
retention-days: 14

publish:
needs: validate
runs-on: ubuntu-latest
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_SHA: ${{ needs.validate.outputs.release_sha }}

steps:
- name: Checkout validated release commit
uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.release_sha }}
fetch-depth: 0

- name: Download verified release assets
uses: actions/download-artifact@v4
with:
name: panelize-${{ inputs.version }}-publish
path: dist/release-${{ inputs.version }}

- name: Revalidate release target
run: |
git fetch --quiet origin main
git merge-base --is-ancestor "$RELEASE_SHA" origin/main

TAG="v${RELEASE_VERSION}"
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
echo "Tag ${TAG} already exists"
exit 1
fi
if gh release view "$TAG" >/dev/null 2>&1; then
echo "GitHub Release ${TAG} already exists"
exit 1
fi

- name: Create annotated release tag
run: |
TAG="v${RELEASE_VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" "$RELEASE_SHA" -m "Panelize ${RELEASE_VERSION}"
git push origin "refs/tags/${TAG}"

- name: Create GitHub Release
run: |
TAG="v${RELEASE_VERSION}"
RELEASE_DIR="dist/release-${RELEASE_VERSION}"
gh release create "$TAG" \
"${RELEASE_DIR}/panelize-${RELEASE_VERSION}-release.zip" \
"${RELEASE_DIR}/panelize-${RELEASE_VERSION}-cws.zip" \
--verify-tag \
--title "Panelize ${RELEASE_VERSION}" \
--notes-file "${RELEASE_DIR}/release-notes.md"

- name: Write publish summary
run: |
{
echo "## Panelize ${RELEASE_VERSION} published"
echo
echo "Tag: v${RELEASE_VERSION}"
echo "Commit: ${RELEASE_SHA}"
echo "Release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/v${RELEASE_VERSION}"
} >> "$GITHUB_STEP_SUMMARY"
116 changes: 66 additions & 50 deletions modules/version-checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,27 @@
// Checks for updates by comparing manifest version with GitHub

import { t } from './i18n.js';
import { compareVersions } from './version-utils.js';

const GITHUB_MANIFEST_URL = 'https://raw.githubusercontent.com/Manho/Panelize/main/manifest.json';
const GITHUB_LATEST_RELEASE_URL = 'https://api.github.com/repos/Manho/Panelize/releases/latest';
const GITHUB_RELEASES_URL = 'https://github.com/Manho/Panelize/releases/latest';

/**
* @typedef {Object} LatestReleaseInfo
* @property {string} version
* @property {string} releaseUrl
* @property {string} downloadUrl
*/

/**
* @typedef {Object} UpdateCheckResult
* @property {boolean} updateAvailable
* @property {string|null} currentVersion
* @property {string|null} latestVersion
* @property {string} releaseUrl
* @property {string} downloadUrl
* @property {string|null} error
*/

/**
* Load local manifest version
Expand All @@ -23,97 +42,94 @@ export async function loadVersionInfo() {
}

/**
* Fetch latest manifest from GitHub
* @returns {Promise<Object|null>} Latest manifest or null on error
* Fetch the latest published release from GitHub.
*
* @returns {Promise<LatestReleaseInfo|null>} Latest release or null on error.
*/
export async function fetchLatestManifest() {
export async function fetchLatestRelease() {
try {
const response = await fetch(GITHUB_MANIFEST_URL, {
const response = await fetch(GITHUB_LATEST_RELEASE_URL, {
headers: {
'Accept': 'application/json'
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
});

if (!response.ok) {
throw new Error(`GitHub fetch error: ${response.status}`);
}

const manifest = await response.json();
return manifest;
const release = await response.json();
const versionMatch = /^v?(\d+\.\d+\.\d+)$/.exec(release.tag_name ?? '');

if (!versionMatch || typeof release.html_url !== 'string') {
throw new Error('GitHub release metadata is invalid');
}

const version = versionMatch[1];
const expectedAssetName = `panelize-${version}-release.zip`;
const releaseAsset = Array.isArray(release.assets)
? release.assets.find(asset => asset?.name === expectedAssetName)
: null;

return {
version,
releaseUrl: release.html_url,
downloadUrl: releaseAsset?.browser_download_url || release.html_url
};
} catch (error) {
console.error('Error fetching latest manifest:', error);
console.error('Error fetching latest GitHub release:', error);
return null;
}
}

/**
* Compare two version strings (e.g., "1.0.0" vs "1.1.0")
* @param {string} current - Current version
* @param {string} latest - Latest version
* @returns {number} -1 if current < latest, 0 if equal, 1 if current > latest
*/
function compareVersions(current, latest) {
const currentParts = current.split('.').map(Number);
const latestParts = latest.split('.').map(Number);

const maxLength = Math.max(currentParts.length, latestParts.length);

for (let i = 0; i < maxLength; i++) {
const currentPart = currentParts[i] || 0;
const latestPart = latestParts[i] || 0;

if (currentPart < latestPart) return -1;
if (currentPart > latestPart) return 1;
}

return 0;
}

/**
* Check if an update is available
* @returns {Promise<Object>} Update status
* @returns {Promise<UpdateCheckResult>} Update status.
*/
export async function checkForUpdates() {
const localInfo = await loadVersionInfo();
if (!localInfo) {
return {
updateAvailable: false,
currentVersion: null,
latestVersion: null,
releaseUrl: GITHUB_RELEASES_URL,
downloadUrl: GITHUB_RELEASES_URL,
error: t('errVersionInfoFailed')
};
}

const latestManifest = await fetchLatestManifest();
if (!latestManifest) {
const latestRelease = await fetchLatestRelease();
if (!latestRelease) {
return {
updateAvailable: false,
currentVersion: localInfo.version,
error: t('errGitHubFetchFailed')
latestVersion: null,
releaseUrl: GITHUB_RELEASES_URL,
downloadUrl: GITHUB_RELEASES_URL,
error: t('msgCheckUpdatesFailed')
};
}

const comparison = compareVersions(localInfo.version, latestManifest.version);
const comparison = compareVersions(localInfo.version, latestRelease.version);
const updateAvailable = comparison < 0;

return {
updateAvailable,
currentVersion: localInfo.version,
latestVersion: latestManifest.version,
latestVersion: latestRelease.version,
releaseUrl: latestRelease.releaseUrl,
downloadUrl: latestRelease.downloadUrl,
error: null
};
}

/**
* Get the download URL for the latest version
* @returns {string} GitHub zip download URL
*/
export function getDownloadUrl() {
return 'https://github.com/Manho/Panelize/archive/refs/heads/main.zip';
}

/**
* Get the GitHub repository URL
* @returns {string} GitHub repository URL
* Get the latest releases page URL.
*
* @returns {string} GitHub releases URL.
*/
export function getRepositoryUrl() {
return 'https://github.com/Manho/Panelize';
export function getReleasesUrl() {
return GITHUB_RELEASES_URL;
}
Loading
Loading