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
231 changes: 196 additions & 35 deletions .github/workflows/release-distribute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ name: Distribute evaOS Beta Release Assets
# evaOS beta fork: manual-only distribution. This workflow must never mirror
# upstream AionUi assets or historical upstream tags.
#
# All credentials / identifiers are read from repository secrets:
# - AWS_REGION
# - AWS_ROLE_ARN
# - AWS_S3_BUCKET
# Do NOT hardcode these values in this file.
# Distribution is GitHub Releases only. The workflow validates the existing
# draft release assets and RC canary proof, then publishes the same draft
# GitHub prerelease. No external bucket or CDN is used for this RC.

on:
# Manual trigger for smoke testing or for retrying a missed release:
Expand Down Expand Up @@ -35,19 +33,21 @@ on:
- all
- macos

permissions:
id-token: write # required for OIDC token
contents: read
actions: read # required for gh run view/download of RC proof artifacts

env:
EVAOS_RELEASE_TARGET_PLATFORMS: ${{ inputs.release_target_platforms || vars.EVAOS_RELEASE_TARGET_PLATFORMS || 'all' }}

concurrency:
group: evaos-beta-distribute-${{ github.event.inputs.tag || github.run_id }}
cancel-in-progress: false

jobs:
distribute:
name: Distribute release assets
runs-on: ubuntu-latest
if: github.event.inputs.beta_distribution_ack == 'evaos-beta' && vars.EVAOS_BETA_RELEASE_PUBLISH_ENABLED == 'true'
permissions:
contents: write # required to publish the validated GitHub prerelease after RC proof
actions: read # required for gh run view/download of RC proof artifacts
steps:
- name: Extract version from tag
id: version
Expand Down Expand Up @@ -106,25 +106,126 @@ jobs:
echo "tag_commit=$TAG_COMMIT" >> $GITHUB_OUTPUT
echo "Validated $TAG at $TAG_COMMIT against ${EVAOS_BETA_RELEASE_BRANCH}."

- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Guard GitHub release is still draft
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
run: |
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,isPrerelease,tagName,publishedAt)
node - "$RELEASE_JSON" <<'NODE'
const release = JSON.parse(process.argv[2]);
if (!release.isDraft) {
throw new Error(`Release ${release.tagName} is already published; refusing same-tag redistribution.`);
}
if (!release.isPrerelease) {
throw new Error(`Release ${release.tagName} must remain marked as a prerelease.`);
}
NODE

- name: Guard against same-version overwrite
- name: Validate GitHub draft asset platform set
env:
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
VERSION: ${{ steps.version.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
run: |
DEST="s3://${S3_BUCKET}/releases/${VERSION}/"
COUNT=$(aws s3 ls "$DEST" 2>/dev/null | wc -l | tr -d ' ')
if [ "$COUNT" -gt 0 ]; then
echo "::error::Version directory already contains $COUNT file(s). Refusing to overwrite."
echo "::error::Same-version re-publish is not allowed (downstream caches would serve stale files). Release a new version instead."
aws s3 ls "$DEST"
exit 1
fi
set -euo pipefail
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets,tagName)
node - "$RELEASE_JSON" "$EVAOS_RELEASE_TARGET_PLATFORMS" <<'NODE'
const fs = require('fs');
const release = JSON.parse(process.argv[2]);
const target = process.argv[3];
const assets = release.assets || [];
if (assets.length === 0) {
throw new Error(`Release ${release.tagName} has no draft assets to publish.`);
}

function normalizeAsset(asset) {
return {
name: asset.name || '',
size: Number(asset.size || 0),
digest: asset.digest || '',
state: asset.state || '',
contentType: asset.contentType || '',
};
}

function writeSnapshot() {
const snapshot = assets.map(normalizeAsset).sort((a, b) => a.name.localeCompare(b.name));
fs.writeFileSync('draft-release-assets.snapshot.json', `${JSON.stringify(snapshot, null, 2)}\n`);
}

if (target === 'all') {
const hasFamily = {
macos: assets.some((asset) => (
/\.(dmg|pkg)$/i.test(asset.name || '') ||
/^latest(?:-arm64)?-mac\.ya?ml$/i.test(asset.name || '') ||
/(^|[._ -])(mac|macos|darwin)([._ -]|$)/i.test(asset.name || '')
)),
windows: assets.some((asset) => (
/\.(exe|msi|appx|appxbundle|msix|msixbundle|nupkg)$/i.test(asset.name || '') ||
/^latest(?:-(?:win|windows)(?:-.+)?)?\.ya?ml$/i.test(asset.name || '') ||
/(^|[._ -])(win|windows)([._ -]|$)/i.test(asset.name || '')
)),
linux: assets.some((asset) => (
/\.(deb|rpm|appimage|snap|pacman)$/i.test(asset.name || '') ||
/\.tar\.(gz|xz|bz2)$/i.test(asset.name || '') ||
/^latest-linux(?:-.+)?\.ya?ml$/i.test(asset.name || '') ||
/(^|[._ -])linux([._ -]|$)/i.test(asset.name || '')
)),
};
const missing = Object.entries(hasFamily)
.filter(([, present]) => !present)
.map(([family]) => family);
if (missing.length > 0) {
throw new Error(`release_target_platforms=all is missing required platform families: ${missing.join(', ')}`);
}
writeSnapshot();
console.log(`All-platform draft asset set accepted for ${release.tagName}.`);
process.exit(0);
}
if (target !== 'macos') {
throw new Error(`Unsupported EVAOS_RELEASE_TARGET_PLATFORMS: ${target}`);
}

const disallowed = [];
for (const asset of assets) {
const name = asset.name || '';
const lower = name.toLowerCase();
const reasons = [];
if (/\.(exe|msi|appx|appxbundle|msix|msixbundle|nupkg)$/i.test(name)) {
reasons.push('Windows installer/package');
}
if (/\.(deb|rpm|appimage|snap|pacman)$/i.test(name)) {
reasons.push('Linux installer/package');
}
if (/\.tar\.(gz|xz|bz2)$/i.test(name)) {
reasons.push('Linux/archive package');
}
if (/^latest(?:-(?:win|windows|linux)(?:-.+)?)?\.ya?ml$/i.test(name)) {
reasons.push('non-macOS updater metadata');
}
if (/\.(exe|msi)\.blockmap$/i.test(name)) {
reasons.push('Windows blockmap');
}
if (/(^|[._ -])(win|windows|linux)([._ -]|$)/i.test(name)) {
reasons.push('non-macOS platform marker');
}
if (lower.includes('appimage')) {
reasons.push('Linux AppImage marker');
}
if (reasons.length > 0) {
disallowed.push(`${name} (${[...new Set(reasons)].join(', ')})`);
}
}

if (disallowed.length > 0) {
throw new Error(
`macOS-only distribution cannot publish non-macOS draft assets for ${release.tagName}:\n` +
disallowed.map((entry) => `- ${entry}`).join('\n')
);
}
writeSnapshot();
console.log(`macOS-only draft asset set accepted for ${release.tagName}: ${assets.map((asset) => asset.name).join(', ')}`);
NODE

- name: Download release assets from GitHub
env:
Expand Down Expand Up @@ -160,7 +261,7 @@ jobs:
;;
esac
gh release download "$TAG" \
--repo "${{ github.repository }}" \
--repo "$GITHUB_REPOSITORY" \
--dir dist \
"${patterns[@]}"
echo "Downloaded files:"
Expand All @@ -185,7 +286,7 @@ jobs:

mkdir -p trusted-manifest
gh run download "$RELEASE_RUN_ID" \
--repo "${{ github.repository }}" \
--repo "$GITHUB_REPOSITORY" \
--name "evaos-beta-release-manifest-${TAG}" \
--dir trusted-manifest
TRUSTED_MANIFEST_PATH=$(find trusted-manifest -type f -name evaos-beta-release-manifest.json | head -n 1)
Expand Down Expand Up @@ -259,7 +360,7 @@ jobs:
exit 1
fi

RUN_JSON=$(gh run view "$RC_PROOF_RUN_ID" --repo "${{ github.repository }}" --json conclusion,event,workflowName)
RUN_JSON=$(gh run view "$RC_PROOF_RUN_ID" --repo "$GITHUB_REPOSITORY" --json conclusion,event,workflowName)
node - "$RUN_JSON" <<'NODE'
const run = JSON.parse(process.argv[2]);
if (run.conclusion !== 'success') {
Expand All @@ -276,7 +377,7 @@ jobs:
rm -rf rc-proof-download rc-proof
mkdir -p rc-proof-download
gh run download "$RC_PROOF_RUN_ID" \
--repo "${{ github.repository }}" \
--repo "$GITHUB_REPOSITORY" \
--name "evaos-beta-rc-proof-${TAG}" \
--dir rc-proof-download
PROOF_MANIFEST=$(find rc-proof-download -type f -name evaos-beta-rc-proof.json | head -n 1)
Expand All @@ -290,13 +391,71 @@ jobs:

node scripts/evaosBetaReleaseGate.js verify-rc-proof rc-proof "$TAG"

- name: Upload assets
- name: Publish GitHub prerelease
env:
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
VERSION: ${{ steps.version.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
run: |
aws s3 cp dist/ "s3://${S3_BUCKET}/releases/${VERSION}/" --recursive
echo "Uploaded release ${VERSION}"
RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets,isDraft,isPrerelease,tagName,publishedAt)
node - "$RELEASE_JSON" <<'NODE'
const fs = require('fs');
const release = JSON.parse(process.argv[2]);
if (!release.isDraft) {
throw new Error(`Release ${release.tagName} is already published; refusing same-tag redistribution.`);
}
if (!release.isPrerelease) {
throw new Error(`Release ${release.tagName} must remain marked as a prerelease.`);
}
if (!fs.existsSync('draft-release-assets.snapshot.json')) {
throw new Error('Missing draft-release-assets.snapshot.json from the asset validation step.');
}

const expected = JSON.parse(fs.readFileSync('draft-release-assets.snapshot.json', 'utf8'));
const current = (release.assets || []).map((asset) => ({
name: asset.name || '',
size: Number(asset.size || 0),
digest: asset.digest || '',
state: asset.state || '',
contentType: asset.contentType || '',
})).sort((a, b) => a.name.localeCompare(b.name));

if (JSON.stringify(current) !== JSON.stringify(expected)) {
const expectedByName = new Map(expected.map((asset) => [asset.name, asset]));
const currentByName = new Map(current.map((asset) => [asset.name, asset]));
const missing = expected.filter((asset) => !currentByName.has(asset.name)).map((asset) => asset.name);
const extra = current.filter((asset) => !expectedByName.has(asset.name)).map((asset) => asset.name);
const changed = current
.filter((asset) => expectedByName.has(asset.name) && JSON.stringify(asset) !== JSON.stringify(expectedByName.get(asset.name)))
.map((asset) => asset.name);
throw new Error(
`Release ${release.tagName} draft assets changed after validation; refusing publish.\n` +
`Missing: ${missing.join(', ') || 'none'}\n` +
`Extra: ${extra.join(', ') || 'none'}\n` +
`Changed metadata: ${changed.join(', ') || 'none'}`
);
}
NODE

gh release edit "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--draft=false \
--prerelease=true \
--latest=false \
--verify-tag

RELEASE_JSON=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,isPrerelease,tagName,publishedAt)
node - "$RELEASE_JSON" <<'NODE'
const release = JSON.parse(process.argv[2]);
if (release.isDraft) {
throw new Error(`Release ${release.tagName} is still a draft after publish.`);
}
if (!release.isPrerelease) {
throw new Error(`Release ${release.tagName} lost prerelease status.`);
}
if (!release.publishedAt) {
throw new Error(`Release ${release.tagName} has no publishedAt timestamp.`);
}
NODE
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Summary
env:
Expand All @@ -308,4 +467,6 @@ jobs:
echo ""
echo "- Tag: \`${TAG}\`"
echo "- Version: \`${VERSION}\`"
echo "- Channel: GitHub Releases prerelease"
echo "- URL: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
} >> $GITHUB_STEP_SUMMARY
87 changes: 87 additions & 0 deletions docs/evaos/public-beta-packaging-rollback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# evaOS Workbench Beta Packaging And Rollback

This document defines the controlled macOS RC packaging, distribution, rollback, and support proof gate for the AionUi-based evaOS Workbench Beta shell.

## Beta Identity

- App name: `evaOS Workbench Beta`
- macOS bundle id: `com.evaos.workbench.beta`
- Executable name: `EvaOSWorkbenchBeta`
- Protocol scheme: `evaos-workbench-beta`
- GitHub release repo: `100yenadmin/evaOS-GUI`
- Release tag prefix: `evaos-beta-`
- Artifact identity marker: `evaOS Workbench Beta`, `EvaOSWorkbenchBeta`, or `evaos-workbench-beta`

The controlled beta must not ship as upstream `AionUi`, use the upstream `iOfficeAI/AionUi` release feed, or publish installer assets with upstream AionUi branding.

## Release Workflow

1. Run `Build and Release` with `beta_release_ack=evaos-beta`.
2. Keep `EVAOS_BETA_RELEASE_PUBLISH_ENABLED` disabled for internal smoke builds.
3. Enable `EVAOS_BETA_RELEASE_PUBLISH_ENABLED=true` only when the release decision is ready for controlled beta distribution.
4. Set `EVAOS_BETA_RELEASE_BRANCH` to the audited release branch before creating public beta tags.
5. Run `evaOS Beta RC Canary` for the same non-dev tag and keep the successful workflow run id.
6. Distribute assets only with `Distribute evaOS Beta Release Assets`, `beta_distribution_ack=evaos-beta`, and a tag that starts with `evaos-beta-`.
7. Do not distribute `-dev-` beta tags. Public distribution requires a non-dev `evaos-beta-` tag reachable from `EVAOS_BETA_RELEASE_BRANCH`.
8. Distribution must validate `evaos-beta-release-manifest.json`, matching asset checksums, the tag commit, the successful `Build and Release` workflow run, and the successful `evaOS Beta RC Canary` proof run before publishing the GitHub prerelease.

No AWS, S3, external bucket, or CDN distribution is required for this controlled RC. The distribution surface is the existing GitHub Release in `100yenadmin/evaOS-GUI`.

## Signing And Notarization

Public beta publishing requires real macOS signing and notarization. Ad-hoc signing is not a distributable release candidate.

Before any signed local or release build, locate the macOS release credential preflight helper and run it. In the Codex operator environment this is provided by the `macos-release-credential-bootstrap` skill; other operators should set the path explicitly.

```bash
export MACOS_RELEASE_CREDENTIAL_PREFLIGHT="/path/to/macos_release_credential_preflight.sh"
"$MACOS_RELEASE_CREDENTIAL_PREFLIGHT" \
--mode bootstrap \
--test-codesign \
--check-notary
```

If a Keychain or signing GUI prompt appears, stop and fix signing/keychain ACLs. Do not ask a user to click through the prompt.

## Updater And Feed Boundary

- `electron-builder` publish config points at `100yenadmin/evaOS-GUI`.
- `publishAutoUpdate` is `false`.
- Releases are created as draft prereleases and are published only after RC canary proof.
- Runtime auto-update is disabled by default for evaOS beta builds.
- Manual update/download surfaces in beta mode must not point at upstream `iOfficeAI/AionUi`.
- Distribution refuses upstream AionUi asset names and non-evaOS beta tags.

## Smoke Proof

Before sharing a controlled beta link, attach proof to the release gate issue:

- Signed macOS arm64 artifact exists.
- Signed macOS x64 artifact exists or x64 is explicitly blocked.
- `codesign --verify --deep --strict --verbose=2 <app>` passes.
- `spctl --assess --type execute --verbose <app>` reports `accepted`.
- Launch smoke proves `com.evaos.workbench.beta` and `evaOS Workbench Beta`.
- Updater/feed audit proves no upstream `iOfficeAI/AionUi` or `aionui.com` feed/support reference in shipped app resources.
- Rollback smoke proves the beta app can be removed and the released fallback `/Applications/evaOS.app` can launch.

## Rollback

The released Workbench app remains the fallback. If a controlled beta artifact fails after publication:

1. Mark the GitHub prerelease as draft or unavailable.
2. Remove the beta app bundle from `/Applications/evaOS Workbench Beta.app`.
3. Restore or launch `/Applications/evaOS.app`.
4. Record whether user data, cache, protocol handler state, and broker login/session state were changed.

## Operator rollback-proof commands

```bash
codesign --verify --deep --strict --verbose=2 "/Applications/evaOS Workbench Beta.app"
spctl --assess --type execute --verbose "/Applications/evaOS Workbench Beta.app"
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -dump | grep -A5 -B5 evaos-workbench-beta
open -n "/Applications/evaOS.app"
```

## Support

Support reports for release candidates must include the tag, app version, bundle id, route, selected customer summary, non-secret audit ids, screenshot/proof folder, signing/notarization/Gatekeeper status, and rollback result.
Loading