mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
ci(release): harden notarized pipeline and automate preflight
This commit is contained in:
parent
3efb496f80
commit
b2420e73b3
19 changed files with 655 additions and 159 deletions
65
.github/workflows/pre-release-ci.yml
vendored
Normal file
65
.github/workflows/pre-release-ci.yml
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
name: Pre-release CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REF: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git init
|
||||
git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
|
||||
git fetch --depth=1 origin "$REF"
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- name: Select/verify Xcode 17+
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/ci/select_xcode17.sh
|
||||
|
||||
- name: Validate basic release docs format
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if grep -nE "^- TODO$" CHANGELOG.md >/dev/null; then
|
||||
echo "CHANGELOG contains TODO entries. Fill release notes before merging." >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -nE "^> Latest release: \\*\\*v[0-9]+\\.[0-9]+\\.[0-9]+(-[A-Za-z0-9.]+)?\\*\\*$" README.md >/dev/null
|
||||
grep -nE "^- Latest release: \\*\\*v[0-9]+\\.[0-9]+\\.[0-9]+(-[A-Za-z0-9.]+)?\\*\\*$" README.md >/dev/null
|
||||
|
||||
- name: Run critical runtime tests
|
||||
env:
|
||||
DERIVED_DATA: ${{ runner.temp }}/DerivedData
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcodebuild \
|
||||
-project "Neon Vision Editor.xcodeproj" \
|
||||
-scheme "Neon Vision Editor" \
|
||||
-destination "platform=macOS" \
|
||||
-derivedDataPath "$DERIVED_DATA" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
-only-testing:"Neon Vision EditorTests/ReleaseRuntimePolicyTests" \
|
||||
test
|
||||
|
||||
- name: Verify icon payload in built app
|
||||
env:
|
||||
DERIVED_DATA: ${{ runner.temp }}/DerivedData
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APP="$DERIVED_DATA/Build/Products/Debug/Neon Vision Editor.app"
|
||||
scripts/ci/verify_icon_payload.sh "$APP"
|
||||
35
.github/workflows/release-dry-run.yml
vendored
Normal file
35
.github/workflows/release-dry-run.yml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
name: Release dry-run
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag/version to validate (e.g. v0.4.9)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dry_run:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REF: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git init
|
||||
git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
|
||||
git fetch --depth=1 origin "$REF"
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- name: Run release preflight
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/ci/release_preflight.sh "$TAG"
|
||||
|
|
@ -60,15 +60,10 @@ jobs:
|
|||
git fetch --depth=1 origin "refs/tags/${TAG_NAME}"
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- name: Verify Xcode version
|
||||
- name: Select/verify Xcode 17+
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcodebuild -version
|
||||
XCODE_MAJOR="$(xcodebuild -version | awk '/Xcode/ {split($2, v, "."); print v[1]}')"
|
||||
if [[ "${XCODE_MAJOR:-0}" -lt 17 ]]; then
|
||||
echo "Xcode 17+ required for AppIcon.icon (dark/light icon composer assets)." >&2
|
||||
exit 1
|
||||
fi
|
||||
scripts/ci/select_xcode17.sh
|
||||
|
||||
- name: Import signing certificate
|
||||
env:
|
||||
|
|
@ -133,25 +128,7 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
APP="$EXPORT_PATH/Neon Vision Editor.app"
|
||||
INFO="$APP/Contents/Info.plist"
|
||||
CAR="$APP/Contents/Resources/Assets.car"
|
||||
|
||||
ICON_NAME="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$INFO" 2>/dev/null || true)"
|
||||
if [[ "$ICON_NAME" != "AppIcon" ]]; then
|
||||
echo "Unexpected CFBundleIconName: '$ICON_NAME' (expected 'AppIcon')." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
xcrun --sdk macosx assetutil --info "$CAR" > icon-info.json
|
||||
if ! grep -Eq '"RenditionName" : "AppIcon\.iconstack"' icon-info.json; then
|
||||
echo "Missing AppIcon iconstack rendition in Assets.car." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Eq '"Name" : "AppIcon"' icon-info.json; then
|
||||
echo "Missing AppIcon image renditions in Assets.car." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Icon payload preflight passed."
|
||||
scripts/ci/verify_icon_payload.sh "$APP"
|
||||
|
||||
- name: Notarize
|
||||
env:
|
||||
|
|
@ -201,6 +178,7 @@ jobs:
|
|||
grep -nE "^### ${TAG_NAME} \\(summary\\)$" README.md >/dev/null
|
||||
|
||||
- name: Create or Update GitHub Release
|
||||
id: publish_release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
|
|
@ -216,6 +194,38 @@ jobs:
|
|||
--notes-file release-notes.md
|
||||
fi
|
||||
|
||||
- name: Verify published release asset payload
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/ci/verify_release_asset.sh "$TAG_NAME"
|
||||
|
||||
- name: Roll back broken published release asset
|
||||
if: ${{ failure() && steps.publish_release.outcome == 'success' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release delete-asset "$TAG_NAME" "Neon.Vision.Editor.app.zip" -y || true
|
||||
gh release edit "$TAG_NAME" --draft true || true
|
||||
echo "Rolled back release asset and marked release as draft due to post-publish verification failure."
|
||||
|
||||
- name: Create release-failure alert issue
|
||||
if: ${{ failure() }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh issue create \
|
||||
--title "Release failure: ${TAG_NAME} (run ${GITHUB_RUN_ID})" \
|
||||
--body "Automated release failed for ${TAG_NAME}. Run URL: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--label "release-alert"
|
||||
|
||||
- name: Trigger homebrew-tap update
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TAP_BOT_TOKEN }}
|
||||
|
|
|
|||
64
.github/workflows/release-notarized.yml
vendored
64
.github/workflows/release-notarized.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
concurrency:
|
||||
group: release-${{ inputs.tag }}
|
||||
cancel-in-progress: true
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-15
|
||||
|
||||
steps:
|
||||
- name: Checkout tag
|
||||
|
|
@ -32,15 +32,10 @@ jobs:
|
|||
git fetch --depth=1 origin "refs/tags/${TAG_NAME}"
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- name: Verify Xcode version
|
||||
- name: Select/verify Xcode 17+
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcodebuild -version
|
||||
XCODE_MAJOR="$(xcodebuild -version | awk '/Xcode/ {split($2, v, "."); print v[1]}')"
|
||||
if [[ "${XCODE_MAJOR:-0}" -lt 17 ]]; then
|
||||
echo "Xcode 17+ required for AppIcon.icon (dark/light icon composer assets)." >&2
|
||||
exit 1
|
||||
fi
|
||||
scripts/ci/select_xcode17.sh
|
||||
|
||||
- name: Import signing certificate
|
||||
env:
|
||||
|
|
@ -105,25 +100,7 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
APP="$EXPORT_PATH/Neon Vision Editor.app"
|
||||
INFO="$APP/Contents/Info.plist"
|
||||
CAR="$APP/Contents/Resources/Assets.car"
|
||||
|
||||
ICON_NAME="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$INFO" 2>/dev/null || true)"
|
||||
if [[ "$ICON_NAME" != "AppIcon" ]]; then
|
||||
echo "Unexpected CFBundleIconName: '$ICON_NAME' (expected 'AppIcon')." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
xcrun --sdk macosx assetutil --info "$CAR" > icon-info.json
|
||||
if ! grep -Eq '"RenditionName" : "AppIcon\.iconstack"' icon-info.json; then
|
||||
echo "Missing AppIcon iconstack rendition in Assets.car." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Eq '"Name" : "AppIcon"' icon-info.json; then
|
||||
echo "Missing AppIcon image renditions in Assets.car." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Icon payload preflight passed."
|
||||
scripts/ci/verify_icon_payload.sh "$APP"
|
||||
|
||||
- name: Notarize
|
||||
env:
|
||||
|
|
@ -173,6 +150,7 @@ jobs:
|
|||
grep -nE "^### ${TAG_NAME} \\(summary\\)$" README.md >/dev/null
|
||||
|
||||
- name: Create or Update GitHub Release
|
||||
id: publish_release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
|
|
@ -188,6 +166,38 @@ jobs:
|
|||
--notes-file release-notes.md
|
||||
fi
|
||||
|
||||
- name: Verify published release asset payload
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/ci/verify_release_asset.sh "$TAG_NAME"
|
||||
|
||||
- name: Roll back broken published release asset
|
||||
if: ${{ failure() && steps.publish_release.outcome == 'success' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release delete-asset "$TAG_NAME" "Neon.Vision.Editor.app.zip" -y || true
|
||||
gh release edit "$TAG_NAME" --draft true || true
|
||||
echo "Rolled back release asset and marked release as draft due to post-publish verification failure."
|
||||
|
||||
- name: Create release-failure alert issue
|
||||
if: ${{ failure() }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh issue create \
|
||||
--title "Release failure: ${TAG_NAME} (run ${GITHUB_RUN_ID})" \
|
||||
--body "Automated release failed for ${TAG_NAME}. Run URL: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--label "release-alert"
|
||||
|
||||
- name: Trigger homebrew-tap update
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TAP_BOT_TOKEN }}
|
||||
|
|
|
|||
68
.github/workflows/release.yml
vendored
68
.github/workflows/release.yml
vendored
|
|
@ -1,26 +1,30 @@
|
|||
name: Release macOS app (unsigned)
|
||||
name: Release macOS app (unsigned dry-run)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Existing Git tag to validate (e.g. v0.4.8)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
concurrency:
|
||||
group: release-${{ github.ref_name }}
|
||||
group: unsigned-dry-run-${{ inputs.tag }}
|
||||
cancel-in-progress: true
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Checkout (no external actions)
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git init
|
||||
git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
|
||||
git fetch --depth=1 origin "refs/tags/${TAG_NAME}"
|
||||
|
|
@ -29,12 +33,7 @@ jobs:
|
|||
- name: Show Xcode Version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcodebuild -version
|
||||
XCODE_MAJOR="$(xcodebuild -version | awk '/Xcode/ {split($2, v, "."); print v[1]}')"
|
||||
if [[ "${XCODE_MAJOR:-0}" -lt 17 ]]; then
|
||||
echo "Xcode 17+ required for AppIcon.icon (dark/light icon composer assets)." >&2
|
||||
exit 1
|
||||
fi
|
||||
scripts/ci/select_xcode17.sh
|
||||
|
||||
- name: Build macOS archive
|
||||
env:
|
||||
|
|
@ -60,25 +59,7 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
APP="$ARCHIVE_PATH/Products/Applications/Neon Vision Editor.app"
|
||||
INFO="$APP/Contents/Info.plist"
|
||||
CAR="$APP/Contents/Resources/Assets.car"
|
||||
|
||||
ICON_NAME="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$INFO" 2>/dev/null || true)"
|
||||
if [[ "$ICON_NAME" != "AppIcon" ]]; then
|
||||
echo "Unexpected CFBundleIconName: '$ICON_NAME' (expected 'AppIcon')." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
xcrun --sdk macosx assetutil --info "$CAR" > icon-info.json
|
||||
if ! grep -Eq '"RenditionName" : "AppIcon\.iconstack"' icon-info.json; then
|
||||
echo "Missing AppIcon iconstack rendition in Assets.car." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Eq '"Name" : "AppIcon"' icon-info.json; then
|
||||
echo "Missing AppIcon image renditions in Assets.car." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Icon payload preflight passed."
|
||||
scripts/ci/verify_icon_payload.sh "$APP"
|
||||
|
||||
- name: Zip app
|
||||
env:
|
||||
|
|
@ -94,14 +75,14 @@ jobs:
|
|||
|
||||
- name: Extract changelog section
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/extract_changelog_section.sh CHANGELOG.md "$TAG_NAME" > release-notes.md
|
||||
|
||||
- name: Validate release docs are in sync
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if grep -nE "^- TODO$" release-notes.md >/dev/null; then
|
||||
|
|
@ -112,16 +93,9 @@ jobs:
|
|||
grep -nE "^- Latest release: \\*\\*${TAG_NAME}\\*\\*$" README.md >/dev/null
|
||||
grep -nE "^### ${TAG_NAME} \\(summary\\)$" README.md >/dev/null
|
||||
|
||||
- name: Create or Update GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
- name: Upload unsigned ZIP as workflow artifact
|
||||
run: |
|
||||
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
|
||||
echo "Release ${TAG_NAME} already exists; skipping unsigned asset upload to avoid clobbering notarized artifacts."
|
||||
else
|
||||
gh release create "$TAG_NAME" \
|
||||
--title "Neon Vision Editor $TAG_NAME" \
|
||||
--notes-file release-notes.md \
|
||||
"Neon.Vision.Editor.app.zip"
|
||||
fi
|
||||
set -euo pipefail
|
||||
mkdir -p unsigned-artifacts
|
||||
cp Neon.Vision.Editor.app.zip unsigned-artifacts/
|
||||
echo "Unsigned dry-run artifact prepared; this workflow never publishes GitHub release assets."
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 188;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
@ -438,7 +438,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 188;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,19 @@
|
|||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "50864E55167D4016920786DF"
|
||||
BuildableName = "Neon Vision EditorTests.xctest"
|
||||
BlueprintName = "Neon Vision EditorTests"
|
||||
ReferencedContainer = "container:Neon Vision Editor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
|
|
|
|||
|
|
@ -99,14 +99,7 @@ struct NeonVisionEditorApp: App {
|
|||
#endif
|
||||
|
||||
private var preferredAppearance: ColorScheme? {
|
||||
switch appearance {
|
||||
case "light":
|
||||
return .light
|
||||
case "dark":
|
||||
return .dark
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
ReleaseRuntimePolicy.preferredColorScheme(for: appearance)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
|
|
|
|||
61
Neon Vision Editor/Core/ReleaseRuntimePolicy.swift
Normal file
61
Neon Vision Editor/Core/ReleaseRuntimePolicy.swift
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum ReleaseRuntimePolicy {
|
||||
static func settingsTab(from requested: String?) -> String {
|
||||
let trimmed = requested?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return trimmed.isEmpty ? "general" : trimmed
|
||||
}
|
||||
|
||||
static func preferredColorScheme(for appearance: String) -> ColorScheme? {
|
||||
switch appearance {
|
||||
case "light":
|
||||
return .light
|
||||
case "dark":
|
||||
return .dark
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func nextFindMatch(
|
||||
in source: String,
|
||||
query: String,
|
||||
useRegex: Bool,
|
||||
caseSensitive: Bool,
|
||||
cursorLocation: Int
|
||||
) -> (range: NSRange, nextCursorLocation: Int)? {
|
||||
guard !query.isEmpty else { return nil }
|
||||
let ns = source as NSString
|
||||
let clampedStart = min(max(0, cursorLocation), ns.length)
|
||||
let forwardRange = NSRange(location: clampedStart, length: max(0, ns.length - clampedStart))
|
||||
let wrapRange = NSRange(location: 0, length: max(0, clampedStart))
|
||||
|
||||
let match: NSRange?
|
||||
if useRegex {
|
||||
guard let regex = try? NSRegularExpression(
|
||||
pattern: query,
|
||||
options: caseSensitive ? [] : [.caseInsensitive]
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
match = regex.firstMatch(in: source, options: [], range: forwardRange)?.range
|
||||
?? regex.firstMatch(in: source, options: [], range: wrapRange)?.range
|
||||
} else {
|
||||
let options: NSString.CompareOptions = caseSensitive ? [] : [.caseInsensitive]
|
||||
match = ns.range(of: query, options: options, range: forwardRange).toOptional()
|
||||
?? ns.range(of: query, options: options, range: wrapRange).toOptional()
|
||||
}
|
||||
|
||||
guard let found = match else { return nil }
|
||||
return (range: found, nextCursorLocation: found.upperBound)
|
||||
}
|
||||
|
||||
static func subscriptionButtonsEnabled(
|
||||
canUseInAppPurchases: Bool,
|
||||
isPurchasing: Bool,
|
||||
isLoadingProducts: Bool
|
||||
) -> Bool {
|
||||
canUseInAppPurchases && !isPurchasing && !isLoadingProducts
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import UIKit
|
|||
|
||||
extension ContentView {
|
||||
func openSettings(tab: String? = nil) {
|
||||
settingsActiveTab = tab ?? "general"
|
||||
settingsActiveTab = ReleaseRuntimePolicy.settingsTab(from: tab)
|
||||
#if os(macOS)
|
||||
if !NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) {
|
||||
_ = NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
|
||||
|
|
@ -202,43 +202,34 @@ extension ContentView {
|
|||
guard !findQuery.isEmpty else { return }
|
||||
findStatusMessage = ""
|
||||
let source = currentContentBinding.wrappedValue
|
||||
let ns = source as NSString
|
||||
let fingerprint = "\(findQuery)|\(findUsesRegex)|\(findCaseSensitive)"
|
||||
if fingerprint != iOSLastFindFingerprint {
|
||||
iOSLastFindFingerprint = fingerprint
|
||||
iOSFindCursorLocation = 0
|
||||
}
|
||||
|
||||
let clampedStart = min(max(0, iOSFindCursorLocation), ns.length)
|
||||
let forwardRange = NSRange(location: clampedStart, length: max(0, ns.length - clampedStart))
|
||||
let wrapRange = NSRange(location: 0, length: max(0, clampedStart))
|
||||
|
||||
let foundRange: NSRange?
|
||||
if findUsesRegex {
|
||||
guard let regex = try? NSRegularExpression(pattern: findQuery, options: findCaseSensitive ? [] : [.caseInsensitive]) else {
|
||||
guard let next = ReleaseRuntimePolicy.nextFindMatch(
|
||||
in: source,
|
||||
query: findQuery,
|
||||
useRegex: findUsesRegex,
|
||||
caseSensitive: findCaseSensitive,
|
||||
cursorLocation: iOSFindCursorLocation
|
||||
) else {
|
||||
if findUsesRegex, (try? NSRegularExpression(pattern: findQuery, options: findCaseSensitive ? [] : [.caseInsensitive])) == nil {
|
||||
findStatusMessage = "Invalid regex pattern"
|
||||
return
|
||||
}
|
||||
foundRange = regex.firstMatch(in: source, options: [], range: forwardRange)?.range
|
||||
?? regex.firstMatch(in: source, options: [], range: wrapRange)?.range
|
||||
} else {
|
||||
let opts: NSString.CompareOptions = findCaseSensitive ? [] : [.caseInsensitive]
|
||||
foundRange = ns.range(of: findQuery, options: opts, range: forwardRange).toOptional()
|
||||
?? ns.range(of: findQuery, options: opts, range: wrapRange).toOptional()
|
||||
}
|
||||
|
||||
guard let match = foundRange else {
|
||||
findStatusMessage = "No matches found"
|
||||
return
|
||||
}
|
||||
|
||||
iOSFindCursorLocation = match.upperBound
|
||||
iOSFindCursorLocation = next.nextCursorLocation
|
||||
NotificationCenter.default.post(
|
||||
name: .moveCursorToRange,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
EditorCommandUserInfo.rangeLocation: match.location,
|
||||
EditorCommandUserInfo.rangeLength: match.length
|
||||
EditorCommandUserInfo.rangeLocation: next.range.location,
|
||||
EditorCommandUserInfo.rangeLength: next.range.length
|
||||
]
|
||||
)
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -194,14 +194,7 @@ struct NeonSettingsView: View {
|
|||
}
|
||||
|
||||
private var preferredColorSchemeOverride: ColorScheme? {
|
||||
switch appearance {
|
||||
case "light":
|
||||
return .light
|
||||
case "dark":
|
||||
return .dark
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
ReleaseRuntimePolicy.preferredColorScheme(for: appearance)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
|
|
|
|||
95
Neon Vision EditorTests/ReleaseRuntimePolicyTests.swift
Normal file
95
Neon Vision EditorTests/ReleaseRuntimePolicyTests.swift
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import XCTest
|
||||
import SwiftUI
|
||||
@testable import Neon_Vision_Editor
|
||||
|
||||
final class ReleaseRuntimePolicyTests: XCTestCase {
|
||||
func testSettingsTabFallsBackToGeneral() {
|
||||
XCTAssertEqual(ReleaseRuntimePolicy.settingsTab(from: nil), "general")
|
||||
XCTAssertEqual(ReleaseRuntimePolicy.settingsTab(from: ""), "general")
|
||||
XCTAssertEqual(ReleaseRuntimePolicy.settingsTab(from: " "), "general")
|
||||
XCTAssertEqual(ReleaseRuntimePolicy.settingsTab(from: "ai"), "ai")
|
||||
}
|
||||
|
||||
func testPreferredColorSchemeMapping() {
|
||||
XCTAssertEqual(ReleaseRuntimePolicy.preferredColorScheme(for: "light"), .light)
|
||||
XCTAssertEqual(ReleaseRuntimePolicy.preferredColorScheme(for: "dark"), .dark)
|
||||
XCTAssertNil(ReleaseRuntimePolicy.preferredColorScheme(for: "system"))
|
||||
XCTAssertNil(ReleaseRuntimePolicy.preferredColorScheme(for: "unknown"))
|
||||
}
|
||||
|
||||
func testFindNextMovesCursorForwardAndWraps() {
|
||||
let text = "alpha beta alpha"
|
||||
let first = ReleaseRuntimePolicy.nextFindMatch(
|
||||
in: text,
|
||||
query: "alpha",
|
||||
useRegex: false,
|
||||
caseSensitive: true,
|
||||
cursorLocation: 0
|
||||
)
|
||||
XCTAssertEqual(first?.range.location, 0)
|
||||
XCTAssertEqual(first?.nextCursorLocation, 5)
|
||||
|
||||
let second = ReleaseRuntimePolicy.nextFindMatch(
|
||||
in: text,
|
||||
query: "alpha",
|
||||
useRegex: false,
|
||||
caseSensitive: true,
|
||||
cursorLocation: first?.nextCursorLocation ?? 0
|
||||
)
|
||||
XCTAssertEqual(second?.range.location, 11)
|
||||
XCTAssertEqual(second?.nextCursorLocation, 16)
|
||||
|
||||
let wrapped = ReleaseRuntimePolicy.nextFindMatch(
|
||||
in: text,
|
||||
query: "alpha",
|
||||
useRegex: false,
|
||||
caseSensitive: true,
|
||||
cursorLocation: second?.nextCursorLocation ?? 0
|
||||
)
|
||||
XCTAssertEqual(wrapped?.range.location, 0)
|
||||
}
|
||||
|
||||
func testFindNextRegexSearch() {
|
||||
let text = "id-12 id-345"
|
||||
let match = ReleaseRuntimePolicy.nextFindMatch(
|
||||
in: text,
|
||||
query: "id-[0-9]+",
|
||||
useRegex: true,
|
||||
caseSensitive: true,
|
||||
cursorLocation: 0
|
||||
)
|
||||
XCTAssertEqual(match?.range.location, 0)
|
||||
XCTAssertEqual(match?.range.length, 5)
|
||||
}
|
||||
|
||||
func testSubscriptionButtonEnablement() {
|
||||
XCTAssertTrue(
|
||||
ReleaseRuntimePolicy.subscriptionButtonsEnabled(
|
||||
canUseInAppPurchases: true,
|
||||
isPurchasing: false,
|
||||
isLoadingProducts: false
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
ReleaseRuntimePolicy.subscriptionButtonsEnabled(
|
||||
canUseInAppPurchases: false,
|
||||
isPurchasing: false,
|
||||
isLoadingProducts: false
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
ReleaseRuntimePolicy.subscriptionButtonsEnabled(
|
||||
canUseInAppPurchases: true,
|
||||
isPurchasing: true,
|
||||
isLoadingProducts: false
|
||||
)
|
||||
)
|
||||
XCTAssertFalse(
|
||||
ReleaseRuntimePolicy.subscriptionButtonsEnabled(
|
||||
canUseInAppPurchases: true,
|
||||
isPurchasing: false,
|
||||
isLoadingProducts: true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
49
scripts/ci/release_preflight.sh
Executable file
49
scripts/ci/release_preflight.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TAG="${1:-}"
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "Usage: scripts/ci/release_preflight.sh <tag>" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$TAG" != v* ]]; then
|
||||
TAG="v$TAG"
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
scripts/ci/select_xcode17.sh
|
||||
|
||||
echo "Validating release docs for $TAG..."
|
||||
./scripts/extract_changelog_section.sh CHANGELOG.md "$TAG" > /tmp/release-notes-"$TAG".md
|
||||
if grep -nE "^- TODO$" /tmp/release-notes-"$TAG".md >/dev/null; then
|
||||
echo "CHANGELOG section for ${TAG} still contains TODO entries." >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -nE "^> Latest release: \\*\\*${TAG}\\*\\*$" README.md >/dev/null
|
||||
grep -nE "^- Latest release: \\*\\*${TAG}\\*\\*$" README.md >/dev/null
|
||||
grep -nE "^### ${TAG} \\(summary\\)$" README.md >/dev/null
|
||||
|
||||
SAFE_TAG="$(echo "$TAG" | tr -c 'A-Za-z0-9_' '_')"
|
||||
WORK_DIR="/tmp/nve_release_preflight_${SAFE_TAG}"
|
||||
DERIVED="${WORK_DIR}/DerivedData"
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
echo "Running critical runtime tests..."
|
||||
xcodebuild \
|
||||
-project "Neon Vision Editor.xcodeproj" \
|
||||
-scheme "Neon Vision Editor" \
|
||||
-destination "platform=macOS" \
|
||||
-derivedDataPath "$DERIVED" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
-only-testing:"Neon Vision EditorTests/ReleaseRuntimePolicyTests" \
|
||||
test >"${WORK_DIR}/test.log"
|
||||
|
||||
APP="$DERIVED/Build/Products/Debug/Neon Vision Editor.app"
|
||||
scripts/ci/verify_icon_payload.sh "$APP"
|
||||
|
||||
echo "Preflight checks passed for $TAG."
|
||||
30
scripts/ci/select_xcode17.sh
Executable file
30
scripts/ci/select_xcode17.sh
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -n "${DEVELOPER_DIR:-}" ]]; then
|
||||
xcodebuild -version
|
||||
fi
|
||||
|
||||
if xcodebuild -version | awk '/Xcode/ {split($2, v, "."); if (v[1] >= 17) exit 0; exit 1}'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for candidate in \
|
||||
/Applications/Xcode_17.*/Contents/Developer \
|
||||
/Applications/Xcode-17.*/Contents/Developer \
|
||||
/Applications/Xcode.app/Contents/Developer
|
||||
do
|
||||
for path in $candidate; do
|
||||
if [[ -d "$path" ]]; then
|
||||
export DEVELOPER_DIR="$path"
|
||||
if xcodebuild -version | awk '/Xcode/ {split($2, v, "."); if (v[1] >= 17) exit 0; exit 1}'; then
|
||||
echo "Using DEVELOPER_DIR=$DEVELOPER_DIR"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "Xcode 17+ is required but not available on this runner." >&2
|
||||
xcodebuild -version || true
|
||||
exit 1
|
||||
45
scripts/ci/verify_icon_payload.sh
Executable file
45
scripts/ci/verify_icon_payload.sh
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APP_PATH="${1:-}"
|
||||
if [[ -z "$APP_PATH" || ! -d "$APP_PATH" ]]; then
|
||||
echo "Usage: scripts/ci/verify_icon_payload.sh <path-to-.app>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INFO="$APP_PATH/Contents/Info.plist"
|
||||
CAR="$APP_PATH/Contents/Resources/Assets.car"
|
||||
|
||||
if [[ ! -f "$INFO" ]]; then
|
||||
echo "Missing Info.plist at $INFO" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$CAR" ]]; then
|
||||
echo "Missing Assets.car at $CAR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ICON_NAME="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$INFO" 2>/dev/null || true)"
|
||||
if [[ "$ICON_NAME" != "AppIcon" ]]; then
|
||||
echo "Unexpected CFBundleIconName: '$ICON_NAME' (expected 'AppIcon')." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_JSON="$(mktemp)"
|
||||
xcrun --sdk macosx assetutil --info "$CAR" > "$TMP_JSON"
|
||||
|
||||
if ! grep -Eq '"RenditionName" : "AppIcon\.iconstack"' "$TMP_JSON"; then
|
||||
echo "Missing AppIcon iconstack rendition in Assets.car." >&2
|
||||
rm -f "$TMP_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Eq '"Name" : "AppIcon"' "$TMP_JSON"; then
|
||||
echo "Missing AppIcon image renditions in Assets.car." >&2
|
||||
rm -f "$TMP_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$TMP_JSON"
|
||||
echo "Icon payload preflight passed."
|
||||
27
scripts/ci/verify_release_asset.sh
Executable file
27
scripts/ci/verify_release_asset.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TAG="${1:-}"
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "Usage: scripts/ci/verify_release_asset.sh <tag>" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$TAG" != v* ]]; then
|
||||
TAG="v$TAG"
|
||||
fi
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "gh CLI is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="/tmp/nve_release_asset_verify_${TAG}"
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
gh release download "$TAG" -p Neon.Vision.Editor.app.zip -D "$WORK_DIR"
|
||||
ditto -x -k "$WORK_DIR/Neon.Vision.Editor.app.zip" "$WORK_DIR/extracted"
|
||||
|
||||
APP="$WORK_DIR/extracted/Neon Vision Editor.app"
|
||||
scripts/ci/verify_icon_payload.sh "$APP"
|
||||
echo "Release asset verification passed for $TAG."
|
||||
37
scripts/configure_branch_protection.sh
Executable file
37
scripts/configure_branch_protection.sh
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO="${1:-h3pdesign/Neon-Vision-Editor}"
|
||||
BRANCH="${2:-main}"
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "gh CLI is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Applying branch protection on ${REPO}:${BRANCH}..."
|
||||
gh api --method PUT "repos/${REPO}/branches/${BRANCH}/protection" \
|
||||
--input - <<'JSON'
|
||||
{
|
||||
"required_status_checks": {
|
||||
"strict": true,
|
||||
"contexts": [
|
||||
"Pre-release CI / preflight"
|
||||
]
|
||||
},
|
||||
"enforce_admins": true,
|
||||
"required_pull_request_reviews": {
|
||||
"dismiss_stale_reviews": true,
|
||||
"require_code_owner_reviews": false,
|
||||
"required_approving_review_count": 1
|
||||
},
|
||||
"restrictions": null,
|
||||
"required_linear_history": true,
|
||||
"allow_force_pushes": false,
|
||||
"allow_deletions": false,
|
||||
"block_creations": false,
|
||||
"required_conversation_resolution": true
|
||||
}
|
||||
JSON
|
||||
|
||||
echo "Branch protection updated successfully."
|
||||
|
|
@ -6,20 +6,22 @@ usage() {
|
|||
Run end-to-end release flow in one command.
|
||||
|
||||
Usage:
|
||||
scripts/release_all.sh <tag> [--date YYYY-MM-DD] [--notarized] [--self-hosted]
|
||||
scripts/release_all.sh <tag> [--date YYYY-MM-DD] [--skip-notarized] [--github-hosted] [--dry-run]
|
||||
|
||||
Examples:
|
||||
scripts/release_all.sh v0.4.6
|
||||
scripts/release_all.sh 0.4.6 --date 2026-02-12
|
||||
scripts/release_all.sh v0.4.6 --notarized
|
||||
scripts/release_all.sh v0.4.6 --notarized --self-hosted
|
||||
scripts/release_all.sh v0.4.9
|
||||
scripts/release_all.sh 0.4.9 --date 2026-02-12
|
||||
scripts/release_all.sh v0.4.9 --github-hosted
|
||||
scripts/release_all.sh v0.4.9 --dry-run
|
||||
|
||||
What it does:
|
||||
1) Prepare README/CHANGELOG docs
|
||||
2) Commit docs changes
|
||||
3) Create annotated tag
|
||||
4) Push main and tag to origin
|
||||
5) (optional) Trigger notarized release workflow (GitHub-hosted by default)
|
||||
1) Run release preflight checks (docs + build + icon payload + tests)
|
||||
2) Prepare README/CHANGELOG docs
|
||||
3) Commit docs changes
|
||||
4) Create annotated tag
|
||||
5) Push main and tag to origin
|
||||
6) Trigger notarized release workflow (self-hosted by default)
|
||||
7) Wait for notarized workflow and verify uploaded release asset payload
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -37,8 +39,9 @@ if [[ "$TAG" != v* ]]; then
|
|||
fi
|
||||
|
||||
DATE_ARG=()
|
||||
TRIGGER_NOTARIZED=0
|
||||
USE_SELF_HOSTED=0
|
||||
TRIGGER_NOTARIZED=1
|
||||
USE_SELF_HOSTED=1
|
||||
DRY_RUN=0
|
||||
|
||||
while [[ "${1:-}" != "" ]]; do
|
||||
case "$1" in
|
||||
|
|
@ -50,11 +53,14 @@ while [[ "${1:-}" != "" ]]; do
|
|||
fi
|
||||
DATE_ARG=(--date "$1")
|
||||
;;
|
||||
--notarized)
|
||||
TRIGGER_NOTARIZED=1
|
||||
--skip-notarized)
|
||||
TRIGGER_NOTARIZED=0
|
||||
;;
|
||||
--self-hosted)
|
||||
USE_SELF_HOSTED=1
|
||||
--github-hosted)
|
||||
USE_SELF_HOSTED=0
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
|
|
@ -70,6 +76,14 @@ if ! command -v gh >/dev/null 2>&1; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running release preflight for ${TAG}..."
|
||||
scripts/ci/release_preflight.sh "$TAG"
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "Dry-run requested. Preflight completed; no commits/tags/workflows were created."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Running release prep for ${TAG}..."
|
||||
prep_cmd=(scripts/release_prep.sh "$TAG")
|
||||
if [[ ${#DATE_ARG[@]} -gt 0 ]]; then
|
||||
|
|
@ -78,22 +92,35 @@ fi
|
|||
prep_cmd+=(--push)
|
||||
"${prep_cmd[@]}"
|
||||
|
||||
echo "Tag push completed. Unsigned release workflow should start automatically."
|
||||
echo "Tag push completed."
|
||||
|
||||
if [[ "$TRIGGER_NOTARIZED" -eq 1 ]]; then
|
||||
echo "Triggering notarized workflow for ${TAG}..."
|
||||
if [[ "$USE_SELF_HOSTED" -eq 1 ]]; then
|
||||
gh workflow run release-notarized-selfhosted.yml -f tag="$TAG" -f use_self_hosted=true
|
||||
echo "Triggered: release-notarized-selfhosted.yml (tag=${TAG}, use_self_hosted=true)"
|
||||
WORKFLOW_NAME="release-notarized-selfhosted.yml"
|
||||
echo "Triggered: ${WORKFLOW_NAME} (tag=${TAG}, use_self_hosted=true)"
|
||||
else
|
||||
gh workflow run release-notarized.yml -f tag="$TAG"
|
||||
echo "Triggered: release-notarized.yml (tag=${TAG})"
|
||||
WORKFLOW_NAME="release-notarized.yml"
|
||||
echo "Triggered: ${WORKFLOW_NAME} (tag=${TAG})"
|
||||
fi
|
||||
|
||||
echo "Waiting for ${WORKFLOW_NAME} run..."
|
||||
sleep 6
|
||||
RUN_ID="$(gh run list --workflow "$WORKFLOW_NAME" --limit 20 --json databaseId,displayTitle --jq ".[] | select(.displayTitle | contains(\"${TAG}\")) | .databaseId" | head -n1)"
|
||||
if [[ -z "$RUN_ID" ]]; then
|
||||
echo "Could not find workflow run for ${TAG}." >&2
|
||||
exit 1
|
||||
fi
|
||||
gh run watch "$RUN_ID"
|
||||
scripts/ci/verify_release_asset.sh "$TAG"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Done."
|
||||
echo "Check runs:"
|
||||
echo " gh run list --workflow release.yml --limit 5"
|
||||
echo " gh run list --workflow pre-release-ci.yml --limit 5"
|
||||
echo " gh run list --workflow release-dry-run.yml --limit 5"
|
||||
echo " gh run list --workflow release-notarized.yml --limit 5"
|
||||
echo " gh run list --workflow release-notarized-selfhosted.yml --limit 5"
|
||||
|
|
|
|||
41
scripts/release_dry_run.sh
Executable file
41
scripts/release_dry_run.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Validate release readiness without creating a tag or pushing.
|
||||
|
||||
Usage:
|
||||
scripts/release_dry_run.sh <tag>
|
||||
|
||||
Example:
|
||||
scripts/release_dry_run.sh v0.4.9
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "" || "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TAG="$1"
|
||||
if [[ "$TAG" != v* ]]; then
|
||||
TAG="v$TAG"
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TMP_WORKTREE="/tmp/nve_release_dry_run_${TAG}_$$"
|
||||
|
||||
git -C "$ROOT" worktree add "$TMP_WORKTREE" HEAD >/dev/null
|
||||
cleanup() {
|
||||
git -C "$ROOT" worktree remove "$TMP_WORKTREE" --force >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
(
|
||||
cd "$TMP_WORKTREE"
|
||||
scripts/ci/release_preflight.sh "$TAG"
|
||||
scripts/release_prep.sh "$TAG"
|
||||
)
|
||||
|
||||
echo "Dry-run finished. Release content for ${TAG} validated in temporary worktree."
|
||||
Loading…
Reference in a new issue