From 85d3b4d99d404fcb2ef8aac516cbe61390c87f04 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Thu, 19 Mar 2026 21:22:52 +0100 Subject: [PATCH 01/29] update orchestrator stable release --- .github/workflows/release.yml | 2 + .github/workflows/unpublish.yml | 83 +++++++++++++++-- RELEASE-HOWTO.md | 11 ++- scripts/release-orchestrator.js | 154 ++++++++++++++++++++++++++++++-- 4 files changed, 237 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02a710e..c9f67aa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,8 @@ on: options: - test - stable + - stable-prepare-pr + - stable-publish config: description: Config file path required: true diff --git a/.github/workflows/unpublish.yml b/.github/workflows/unpublish.yml index 5bb8a08..cbf111e 100644 --- a/.github/workflows/unpublish.yml +++ b/.github/workflows/unpublish.yml @@ -11,6 +11,20 @@ on: description: Exact version to unpublish (e.g. 4.0.5) required: true type: string + action: + description: Choose operation (unpublish, deprecate, or auto fallback) + required: true + default: auto + type: choice + options: + - auto + - unpublish + - deprecate + deprecate_message: + description: Message used when deprecating (required for deprecate/auto fallback) + required: true + default: "Deprecated due to release rollback. Please use the latest stable version." + type: string dry_run: description: Dry run only (do not execute unpublish) required: true @@ -52,29 +66,88 @@ jobs: - name: Show target run: | echo "Target: ${{ inputs.package_name }}@${{ inputs.version }}" + echo "Action: ${{ inputs.action }}" echo "Dry run: ${{ inputs.dry_run }}" - name: Dry run check if: ${{ inputs.dry_run }} run: | echo "Dry run enabled. No unpublish command executed." - echo "Would run: npm unpublish ${{ inputs.package_name }}@${{ inputs.version }} --force" + if [ "${{ inputs.action }}" = "deprecate" ]; then + echo "Would run: npm deprecate -f '${{ inputs.package_name }}@${{ inputs.version }}' '${{ inputs.deprecate_message }}'" + elif [ "${{ inputs.action }}" = "unpublish" ]; then + echo "Would run: npm unpublish ${{ inputs.package_name }}@${{ inputs.version }} --force" + else + echo "Would run: npm unpublish ${{ inputs.package_name }}@${{ inputs.version }} --force" + echo "If blocked by npm policy, would run: npm deprecate -f '${{ inputs.package_name }}@${{ inputs.version }}' '${{ inputs.deprecate_message }}'" + fi - - name: Unpublish exact version + - name: Execute package retirement action if: ${{ !inputs.dry_run }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | - npm unpublish ${{ inputs.package_name }}@${{ inputs.version }} --force + set -euo pipefail + target="${{ inputs.package_name }}@${{ inputs.version }}" + message="${{ inputs.deprecate_message }}" + + if [ "${{ inputs.action }}" = "deprecate" ]; then + npm deprecate -f "$target" "$message" + exit 0 + fi + + if [ "${{ inputs.action }}" = "unpublish" ]; then + npm unpublish "$target" --force + exit 0 + fi + + set +e + output=$(npm unpublish "$target" --force 2>&1) + status=$? + set -e - - name: Verify version is no longer available + echo "$output" + if [ $status -eq 0 ]; then + echo "Unpublish succeeded." + exit 0 + fi + + if echo "$output" | grep -qi "You can no longer unpublish this package\|Failed criteria\|E405"; then + echo "Unpublish blocked by npm policy; falling back to deprecate..." + npm deprecate -f "$target" "$message" + exit 0 + fi + + echo "Unpublish failed for a reason other than npm policy; not falling back." + exit $status + + - name: Verify outcome if: ${{ !inputs.dry_run }} run: | + if [ "${{ inputs.action }}" = "deprecate" ]; then + npm view ${{ inputs.package_name }}@${{ inputs.version }} deprecated + exit 0 + fi + + if [ "${{ inputs.action }}" = "auto" ]; then + # In auto mode, either unpublish (version no longer resolves) or deprecate (deprecated message exists) is acceptable. + set +e + npm view ${{ inputs.package_name }}@${{ inputs.version }} version >/dev/null 2>&1 + exists_status=$? + set -e + if [ $exists_status -ne 0 ]; then + echo "Version no longer resolvable (unpublished)." + exit 0 + fi + npm view ${{ inputs.package_name }}@${{ inputs.version }} deprecated + exit 0 + fi + set +e npm view ${{ inputs.package_name }}@${{ inputs.version }} version status=$? if [ $status -eq 0 ]; then - echo "Version still resolvable from npm registry." + echo "Version still resolvable from npm registry (unpublish did not occur)." exit 1 fi echo "Unpublish appears successful (version not resolvable)." diff --git a/RELEASE-HOWTO.md b/RELEASE-HOWTO.md index 24f8574..e7adb7a 100644 --- a/RELEASE-HOWTO.md +++ b/RELEASE-HOWTO.md @@ -40,11 +40,13 @@ Before the release workflow can publish stable versions from GitHub Actions: |----------|---------|-------|-----------| | **Local Testing** | `node scripts/release-orchestrator.js --dry-run=true` | Your computer | ❌ No (prints what would happen) | | **Test Release** | Manual trigger: mode=test | GitHub Actions | ✅ Yes (@test tag) | -| **Stable Release** | Manual trigger: mode=stable | GitHub Actions | ✅ Yes (@latest tag) | +| **Stable Release (Direct Push)** | Manual trigger: mode=stable | GitHub Actions | ✅ Yes (@latest tag) | +| **Stable Prepare PR** | Manual trigger: mode=stable-prepare-pr | GitHub Actions | ❌ No (creates release branch + PR) | +| **Stable Publish After Merge** | Manual trigger: mode=stable-publish | GitHub Actions | ✅ Yes (@latest tag) | **Workflow:** ``` -You click "Run workflow" button in GitHub Actions (mode=test or mode=stable) +You click "Run workflow" button in GitHub Actions (mode=test / stable / stable-prepare-pr / stable-publish) ↓ release.yml starts ↓ @@ -74,6 +76,11 @@ For each repo listed: Generates release-summary.json ``` + Two-step stable flow for protected main branches: + 1. Run `mode=stable-prepare-pr` to create and push `release/*` branches and open PRs to `main`. + 2. Merge the PRs via your normal protected-branch process. + 3. Run `mode=stable-publish` to publish from merged `main` without direct push to `main`. + ## Key Points - **Individual repos need nothing special** — they just need `package.json` and npm scripts diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 396f8b2..5efa9b9 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -230,6 +230,119 @@ function withCiGitAuth(url) { return `https://x-access-token:${token}@github.com/${match[1]}`; } +function parseGitHubRepoSlug(url) { + const raw = String(url || '').trim(); + if (!raw) return null; + const withoutSuffix = raw.replace(/\.git$/i, ''); + const match = withoutSuffix.match(/github\.com[:/]([^/]+\/[^/]+)$/i); + return match && match[1] ? match[1] : null; +} + +function buildReleaseBranchName(repoName) { + const stamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 12); + const sanitized = String(repoName || 'repo').toLowerCase().replace(/[^a-z0-9._-]+/g, '-'); + return `release/${sanitized}-${stamp}`; +} + +function maybeCreatePullRequest(repoDir, repo, baseBranch, headBranch, dryRun) { + const slug = parseGitHubRepoSlug(repo.repo || runQuiet('git remote get-url origin', repoDir)); + const title = `Release: merge ${headBranch} into ${baseBranch}`; + const body = 'Automated stable release preparation.'; + + if (dryRun) { + console.log(`[dry-run] Would create PR for ${slug || repo.name}: ${headBranch} -> ${baseBranch}`); + return; + } + + try { + runQuiet('gh --version', repoDir); + } catch (err) { + console.log('gh CLI not available in runner. Skipping automatic PR creation.'); + return; + } + + try { + // If PR already exists, gh returns non-zero; we handle that below. + const repoArg = slug ? `--repo ${slug}` : ''; + run(`gh pr create ${repoArg} --base ${baseBranch} --head ${headBranch} --title "${title}" --body "${body}"`.trim(), repoDir, dryRun); + } catch (err) { + console.log(`PR create skipped for ${repo.name}: ${err.message}`); + } +} + +function prepareStablePullRequest(repoDir, repo, config, branch, dryRun, branchOverride) { + const skipIfNoDiff = repo.skipIfNoDiff ?? config.skipIfNoDiff ?? true; + const shouldCheckDiff = !dryRun && skipIfNoDiff && !branchOverride; + const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; + + try { + runQuiet(`git fetch origin ${devBranch}:refs/remotes/origin/${devBranch}`, repoDir); + } catch (err) { + console.log(`Warning: Could not fetch ${devBranch}: ${err.message}`); + } + + const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${devBranch}`, repoDir)) || 0; + if (shouldCheckDiff && commitsAhead === 0) { + return { status: 'skipped', reason: 'no-diff' }; + } + + const releaseBranch = buildReleaseBranchName(repo.name); + ensureCiPushAccess(repoDir, releaseBranch, dryRun, { gitPush: true }); + run(`git switch -c ${releaseBranch}`, repoDir, dryRun); + run(`git merge origin/${devBranch} -m "Merge ${devBranch} into ${branch} for release [skip ci]"`, repoDir, dryRun); + run(`git push -u origin ${releaseBranch}`, repoDir, dryRun); + maybeCreatePullRequest(repoDir, repo, branch, releaseBranch, dryRun); + + return { + status: dryRun ? 'dry-run' : 'prepared-pr', + releaseBranch, + sourceBranch: devBranch, + targetBranch: branch + }; +} + +function ensureMainContainsStableChanges(repoDir, config, branch, dryRun) { + if (dryRun) return; + const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; + try { + runQuiet(`git fetch origin ${devBranch}:refs/remotes/origin/${devBranch}`, repoDir); + const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${devBranch}`, repoDir)) || 0; + if (commitsAhead > 0) { + throw new Error( + `${commitsAhead} commit(s) still in ${devBranch} but not in ${branch}. ` + + 'Merge the release PR into main before running mode=stable-publish.' + ); + } + } catch (err) { + if (err.message.includes('Merge the release PR')) { + throw err; + } + console.log(`Warning: Could not validate ${devBranch} vs ${branch}: ${err.message}`); + } +} + +function ensureCiPushAccess(repoDir, branch, dryRun, modeConfig = {}) { + if (process.env.GITHUB_ACTIONS !== 'true') return; + if (dryRun) return; + if (modeConfig.gitPush === false) return; + + const tokenSource = process.env.GIT_PUSH_TOKEN ? 'GIT_PUSH_TOKEN' : (process.env.GITHUB_TOKEN ? 'GITHUB_TOKEN' : null); + if (!tokenSource) { + throw new Error(`No git push token available for ${repoDir}. Configure GIT_PUSH_TOKEN or GITHUB_TOKEN in CI.`); + } + + try { + runQuiet(`git push --dry-run origin HEAD:${branch}`, repoDir); + } catch (err) { + throw new Error( + `Push access check failed for branch '${branch}'. ` + + `The CI job is authenticated with ${tokenSource}, but that token cannot push to this repository/branch. ` + + `Use a fine-grained PAT in GIT_PUSH_TOKEN with Contents: Read and write on the target SolidOS repos, ` + + `and ensure branch protection allows that token/account to push.` + ); + } +} + function getAheadBehind(repoDir, branch) { const raw = runQuiet(`git rev-list --left-right --count origin/${branch}...HEAD`, repoDir); const [behind, ahead] = raw.split('\t').map(Number); @@ -801,7 +914,16 @@ function main() { const config = readJson(configPath); const configDir = path.dirname(configPath); - const modeConfig = getModeConfig(config, mode); + const isStablePrepareMode = mode === 'stable-prepare-pr'; + const isStablePublishMode = mode === 'stable-publish'; + const isStableMode = mode === 'stable'; + const modeConfigBase = getModeConfig(config, mode) || {}; + const stableBaseConfig = getModeConfig(config, 'stable') || {}; + const modeConfig = isStablePrepareMode + ? { ...stableBaseConfig, ...modeConfigBase, gitPush: false, gitTag: false } + : isStablePublishMode + ? { ...stableBaseConfig, ...modeConfigBase, gitPush: false, gitTag: false } + : modeConfigBase; if (!config.repos || !Array.isArray(config.repos)) { throw new Error('Config must include a repos array.'); @@ -843,6 +965,26 @@ function main() { } ensureCiGitSetup(repoDir, dryRun); + if (isStableMode) { + ensureCiPushAccess(repoDir, branch, dryRun, effectiveModeConfig); + } + + if (isStablePrepareMode) { + const prepareResult = prepareStablePullRequest(repoDir, repo, config, branch, dryRun, branchOverride); + summary.push({ + name: repo.name, + status: prepareResult.status, + reason: prepareResult.reason || null, + releaseBranch: prepareResult.releaseBranch || null, + sourceBranch: prepareResult.sourceBranch || null, + targetBranch: prepareResult.targetBranch || null + }); + continue; + } + + if (isStablePublishMode) { + ensureMainContainsStableChanges(repoDir, config, branch, dryRun); + } // Skip ahead/behind check in dry-run since git commands don't actually execute if (!dryRun) { @@ -868,7 +1010,7 @@ function main() { // Stable mode skips if no diff, unless branch was explicitly specified or dry-run let shouldMergeDev = false; - if (mode === 'stable') { + if (isStableMode) { const skipIfNoDiff = repo.skipIfNoDiff ?? config.skipIfNoDiff ?? true; const shouldCheckDiff = !dryRun && skipIfNoDiff && !branchOverride; @@ -902,7 +1044,7 @@ function main() { } // For stable mode: check if we need to merge dev (even if skipIfNoDiff is disabled) - if (mode === 'stable' && !shouldMergeDev) { + if (isStableMode && !shouldMergeDev) { const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; try { @@ -919,7 +1061,7 @@ function main() { } // Merge dev into main before publishing (stable mode only) - if (mode === 'stable' && shouldMergeDev) { + if (isStableMode && shouldMergeDev) { const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; console.log(`Merging origin/${devBranch} into ${branch}...`); try { @@ -930,7 +1072,7 @@ function main() { } // Stable mode: lock dependency versions based on what afterInstall actually installed. - if (mode === 'stable') { + if (isStableMode || isStablePublishMode) { lockStableDependencyVersions(repoDir, repo, config, effectiveModeConfig, dryRun); } @@ -969,7 +1111,7 @@ function main() { tag: result.tag, publishedAs: result.packageName ? `${result.packageName}@${result.version}` : null }); - } else if (mode === 'stable') { + } else if (isStableMode || isStablePublishMode) { const result = publishStable(repoDir, effectiveModeConfig, dryRun, effectiveBuildCmd); summary.push({ name: repo.name, From 11b68e1a63128b75a67e06b94e899a5a571648fd Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Thu, 19 Mar 2026 21:42:55 +0100 Subject: [PATCH 02/29] updated --- RELEASE-HOWTO.md | 77 ++++++++++---------- scripts/release-orchestrator.js | 123 +++++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 39 deletions(-) diff --git a/RELEASE-HOWTO.md b/RELEASE-HOWTO.md index e7adb7a..2a6a8af 100644 --- a/RELEASE-HOWTO.md +++ b/RELEASE-HOWTO.md @@ -27,10 +27,12 @@ Before the release workflow can publish stable versions from GitHub Actions: 3. In `solidos/solidos` → Settings → Secrets and variables → Actions, add: - `GIT_PUSH_TOKEN` - `NPM_TOKEN` -4. Confirm target repos allow the token/account to push to `main`, or decide to use a PR-based merge flow instead. -5. Run the `Solidos Release` workflow manually with: +4. For **protected branches on main**: Use `mode=stable-publish` (no extra config needed—it auto-merges PRs) +5. For **unprotected main branch**: Use `mode=stable` (requires direct push access) +6. Run the `Solidos Release` workflow manually with: - `mode=test` for prerelease publishing - - `mode=stable` for `@latest` + - `mode=stable-publish` for `@latest` (protected branch) + - `mode=stable` for `@latest` (direct push) ## How It Works @@ -41,45 +43,29 @@ Before the release workflow can publish stable versions from GitHub Actions: | **Local Testing** | `node scripts/release-orchestrator.js --dry-run=true` | Your computer | ❌ No (prints what would happen) | | **Test Release** | Manual trigger: mode=test | GitHub Actions | ✅ Yes (@test tag) | | **Stable Release (Direct Push)** | Manual trigger: mode=stable | GitHub Actions | ✅ Yes (@latest tag) | -| **Stable Prepare PR** | Manual trigger: mode=stable-prepare-pr | GitHub Actions | ❌ No (creates release branch + PR) | -| **Stable Publish After Merge** | Manual trigger: mode=stable-publish | GitHub Actions | ✅ Yes (@latest tag) | +| **Stable Release (Protected Branch)** | Manual trigger: mode=stable-publish | GitHub Actions | ✅ Yes (@latest tag, auto-merges PR) | -**Workflow:** +**Workflow (stable-publish mode with protected branches):** ``` -You click "Run workflow" button in GitHub Actions (mode=test / stable / stable-prepare-pr / stable-publish) - ↓ -release.yml starts - ↓ -Runs: node scripts/release-orchestrator.js --mode ... - ↓ -Script reads release.config.json (list of repos to release) - ↓ +You click "Run workflow" → mode=stable-publish + ↓ Step 1/3: Create Release PR +Creates release branch from dev → Merges dev into main → Pushes branch → Opens PR + ↓ Step 2/3: Auto-Merge PR +Waits for CI checks to pass → Auto-merges PR with --squash → Fetches updated main + ↓ Step 3/3: Publish For each repo listed: - - Clone if missing (optional) - - Checkout branch (dev for test, main for stable) - npm install - - afterInstall with @test or @latest tags (with fallback) - - [Stable mode only: Check skip logic] - - Compare origin/dev vs main - - If dev has new commits → merge origin/dev into main with [skip ci] - - If no changes and --branch not specified → skip this repo - - [Test mode: always continues] - - - npm test - - npm run build - - npm version (bump patch/minor/major/prerelease) - - npm publish (to npm registry with @test or @latest tag) - - git push + tags (stable only) - ↓ -Generates release-summary.json + - npm version (bump patch/minor/major) + - npm publish (@latest tag) + - git push + tags all at once ``` - Two-step stable flow for protected main branches: - 1. Run `mode=stable-prepare-pr` to create and push `release/*` branches and open PRs to `main`. - 2. Merge the PRs via your normal protected-branch process. - 3. Run `mode=stable-publish` to publish from merged `main` without direct push to `main`. +**Old two-step flow (still supported for manual workflows):** +- Step 1: `mode=stable-prepare-pr` - creates PR, human reviews/merges +- Step 2: `mode=stable-publish` - requires manual merge first + +**New unified flow (recommended):** +- Single `mode=stable-publish` - does all three: create PR, auto-merge, publish ## Key Points @@ -122,7 +108,21 @@ node scripts/release-orchestrator.js --mode test --dry-run=true - **Always publishes** (no skip logic) - **Use case:** Pre-release versions for testing from dev branch -**Scenario 3: GitHub Stable Release** +**Scenario 3: GitHub Stable Release (Protected Branches)** +- Click Actions → "Solidos Release" → Run workflow +- Inputs: mode=stable-publish, dry_run=false +- **Single command that does everything:** + 1. Creates release branch from dev + 2. Opens PR to main + 3. Waits for CI checks to pass + 4. Auto-merges PR (squash merge) + 5. Publishes all packages to npm with `@latest` tag + 6. Pushes git tags to main +- Eliminates manual PR merge step +- **Perfect for:** Organizations with branch protection rules on `main` +- **Use case:** Automated stable releases without human intervention on PR merge + +**Scenario 4: GitHub Stable Release (Direct Push)** - Click Actions → "Solidos Release" → Run workflow - Inputs: mode=stable, dry_run=false - Automatically merges origin/dev → main if dev has new commits @@ -130,7 +130,8 @@ node scripts/release-orchestrator.js --mode test --dry-run=true - Creates git tags and pushes to GitHub - Results in GitHub Actions logs and artifacts - Skips if dev has no new commits (unless --branch=main specified) -- **Use case:** Production releases to @latest +- **WARNING:** Requires write access to protected branches, may fail with 403 +- **Use case:** Repositories without branch protection on `main` Local dry-run - Show the exact commands without running them: diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 5efa9b9..9c4ce56 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -321,6 +321,100 @@ function ensureMainContainsStableChanges(repoDir, config, branch, dryRun) { } } +function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun) { + if (dryRun) { + console.log(`[dry-run] Would wait for and merge PR: ${headBranch} -> ${baseBranch}`); + return { status: 'dry-run', prNumber: null }; + } + + const slug = parseGitHubRepoSlug(repo.repo || runQuiet('git remote get-url origin', repoDir)); + if (!slug) { + console.log('Warning: Could not determine GitHub repo slug. Skipping PR merge.'); + return { status: 'skip', reason: 'no-slug' }; + } + + try { + runQuiet('gh --version', repoDir); + } catch (err) { + console.log('gh CLI not available in runner. Skipping automatic PR merge.'); + return { status: 'skip', reason: 'no-gh-cli' }; + } + + try { + // Find the PR number for this head branch + const prQuery = `gh pr list --repo ${slug} --head ${headBranch} --base ${baseBranch} --state open --json number --jq '.[0].number'`; + let prNumber; + try { + prNumber = parseInt(runQuiet(prQuery, repoDir), 10); + } catch (err) { + console.log(`No open PR found for ${headBranch} -> ${baseBranch}`); + return { status: 'skip', reason: 'no-pr' }; + } + + if (!prNumber) { + console.log(`No open PR found for ${headBranch} -> ${baseBranch}`); + return { status: 'skip', reason: 'no-pr' }; + } + + console.log(`Found PR #${prNumber}. Waiting for checks to pass...`); + + // Wait for checks (with timeout of 10 minutes) + const maxWaitTime = 10 * 60 * 1000; + const checkInterval = 10 * 1000; + const startTime = Date.now(); + let checksComplete = false; + + while (!checksComplete && (Date.now() - startTime) < maxWaitTime) { + try { + const statusQuery = `gh pr view ${prNumber} --repo ${slug} --json statusCheckRollup --jq '.statusCheckRollup[0].status // "PENDING"'`; + const status = runQuiet(statusQuery, repoDir).trim(); + console.log(` PR #${prNumber} status: ${status}`); + + if (status === 'SUCCESS') { + checksComplete = true; + } else if (status === 'FAILURE') { + throw new Error(`PR #${prNumber} checks failed`); + } else { + // PENDING or unknown, wait and retry + console.log(` Waiting ${checkInterval / 1000}s before next check...`); + if (!dryRun) { + // Only sleep if not dry-run + const now = Date.now(); + while (Date.now() - now < checkInterval) { + // Busy wait (or use setTimeout in a real implementation) + } + } + } + } catch (err) { + console.log(` Warning checking PR status: ${err.message}`); + if (!dryRun) { + const now = Date.now(); + while (Date.now() - now < checkInterval) { + // Busy wait + } + } + } + } + + if (!checksComplete) { + console.warn(`Checks did not pass within ${maxWaitTime / 1000 / 60} minutes. Attempting merge anyway...`); + } + + console.log(`Merging PR #${prNumber}...`); + run(`gh pr merge ${prNumber} --repo ${slug} --squash --auto --delete-branch`.trim(), repoDir, dryRun); + + // Fetch the updated main branch + run(`git fetch origin ${baseBranch}`, repoDir, dryRun); + run(`git checkout ${baseBranch}`, repoDir, dryRun); + run(`git pull origin ${baseBranch}`, repoDir, dryRun); + + return { status: 'merged', prNumber }; + } catch (err) { + console.error(`Error during PR merge: ${err.message}`); + throw err; + } +} + function ensureCiPushAccess(repoDir, branch, dryRun, modeConfig = {}) { if (process.env.GITHUB_ACTIONS !== 'true') return; if (dryRun) return; @@ -983,7 +1077,34 @@ function main() { } if (isStablePublishMode) { - ensureMainContainsStableChanges(repoDir, config, branch, dryRun); + // New unified flow: prepare PR, wait for merge, then publish + const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; + + // Step 1: Create release branch and PR + console.log(`Step 1/3: Creating release PR from ${devBranch} -> ${branch}...`); + const prepareResult = prepareStablePullRequest(repoDir, repo, config, branch, dryRun, branchOverride); + + if (prepareResult.status === 'skipped') { + console.log(`Skipping ${repo.name}: ${prepareResult.reason}`); + summary.push({ + name: repo.name, + status: 'skipped', + reason: prepareResult.reason || null + }); + continue; + } + + // Step 2: Wait for PR merge (with auto-merge) + console.log(`Step 2/3: Merging release PR...`); + const mergeResult = waitForPRMerge(repoDir, repo, prepareResult.releaseBranch, branch, dryRun); + + if (mergeResult.status === 'skip') { + console.log(`Warning: PR merge skipped (${mergeResult.reason}). Continuing with publish...`); + } + + // Step 3: Continue with publish (no need for ensureMainContainsStableChanges since we just merged) + console.log(`Step 3/3: Publishing packages...`); + // Fall through to regular publish logic below } // Skip ahead/behind check in dry-run since git commands don't actually execute From 485f886a0f6af05ad4d8ee88f9aa632e95a2ee45 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Thu, 19 Mar 2026 23:34:39 +0100 Subject: [PATCH 03/29] update stable-publish --- scripts/release-orchestrator.js | 224 ++++++++++++++++++++++---------- 1 file changed, 156 insertions(+), 68 deletions(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 9c4ce56..0896373 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -152,6 +152,42 @@ function getModeBranch(config, modeName, fallback = null) { return fallback; } +function remoteBranchExists(repoDir, branch) { + if (!branch) return false; + try { + const result = runQuiet(`git ls-remote --heads origin ${branch}`, repoDir); + return Boolean(result); + } catch (err) { + return false; + } +} + +function getReleaseSourceBranch(repoDir, repo, config, targetBranch, dryRun) { + const stablePublishConfig = getModeConfig(config, 'stable-publish'); + const stableConfig = getModeConfig(config, 'stable'); + const configuredSourceBranch = + repo.releaseSourceBranch || + repo.sourceBranch || + stablePublishConfig.sourceBranch || + stableConfig.sourceBranch || + config.releaseSourceBranch || + getModeBranch(config, 'test', null); + + if (dryRun) { + return configuredSourceBranch || targetBranch; + } + + if (configuredSourceBranch && remoteBranchExists(repoDir, configuredSourceBranch)) { + return configuredSourceBranch; + } + + if (configuredSourceBranch && configuredSourceBranch !== targetBranch) { + console.log(`Warning: Release source branch '${configuredSourceBranch}' not found on origin. Falling back to '${targetBranch}'.`); + } + + return targetBranch; +} + function hasScript(pkg, scriptName) { return !!(pkg && pkg.scripts && pkg.scripts[scriptName]); } @@ -244,44 +280,69 @@ function buildReleaseBranchName(repoName) { return `release/${sanitized}-${stamp}`; } -function maybeCreatePullRequest(repoDir, repo, baseBranch, headBranch, dryRun) { +function maybeCreatePullRequest(repoDir, repo, baseBranch, headBranch, dryRun, options = {}) { const slug = parseGitHubRepoSlug(repo.repo || runQuiet('git remote get-url origin', repoDir)); const title = `Release: merge ${headBranch} into ${baseBranch}`; const body = 'Automated stable release preparation.'; + const required = toBool(options.required, false); if (dryRun) { console.log(`[dry-run] Would create PR for ${slug || repo.name}: ${headBranch} -> ${baseBranch}`); - return; + return { status: 'dry-run' }; } try { runQuiet('gh --version', repoDir); } catch (err) { + if (required) { + throw new Error('gh CLI not available in runner. Cannot create required release PR.'); + } console.log('gh CLI not available in runner. Skipping automatic PR creation.'); - return; + return { status: 'skipped', reason: 'no-gh-cli' }; } try { - // If PR already exists, gh returns non-zero; we handle that below. const repoArg = slug ? `--repo ${slug}` : ''; run(`gh pr create ${repoArg} --base ${baseBranch} --head ${headBranch} --title "${title}" --body "${body}"`.trim(), repoDir, dryRun); + return { status: 'created' }; } catch (err) { + if (required) { + throw new Error(`PR create failed for ${repo.name}: ${err.message}`); + } console.log(`PR create skipped for ${repo.name}: ${err.message}`); + return { status: 'skipped', reason: 'create-failed' }; } } +function createReleaseBranch(repoDir, repo, baseBranch, dryRun) { + const releaseBranch = buildReleaseBranchName(repo.name); + ensureCiPushAccess(repoDir, releaseBranch, dryRun, { gitPush: true }); + run(`git switch -c ${releaseBranch}`, repoDir, dryRun); + return releaseBranch; +} + function prepareStablePullRequest(repoDir, repo, config, branch, dryRun, branchOverride) { const skipIfNoDiff = repo.skipIfNoDiff ?? config.skipIfNoDiff ?? true; const shouldCheckDiff = !dryRun && skipIfNoDiff && !branchOverride; - const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; + const sourceBranch = getReleaseSourceBranch(repoDir, repo, config, branch, dryRun); + + if (sourceBranch === branch) { + return { + status: dryRun ? 'dry-run' : 'skipped', + reason: dryRun ? null : 'source-equals-target', + releaseBranch: null, + sourceBranch, + targetBranch: branch + }; + } try { - runQuiet(`git fetch origin ${devBranch}:refs/remotes/origin/${devBranch}`, repoDir); + runQuiet(`git fetch origin ${sourceBranch}:refs/remotes/origin/${sourceBranch}`, repoDir); } catch (err) { - console.log(`Warning: Could not fetch ${devBranch}: ${err.message}`); + console.log(`Warning: Could not fetch ${sourceBranch}: ${err.message}`); } - const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${devBranch}`, repoDir)) || 0; + const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${sourceBranch}`, repoDir)) || 0; if (shouldCheckDiff && commitsAhead === 0) { return { status: 'skipped', reason: 'no-diff' }; } @@ -289,27 +350,28 @@ function prepareStablePullRequest(repoDir, repo, config, branch, dryRun, branchO const releaseBranch = buildReleaseBranchName(repo.name); ensureCiPushAccess(repoDir, releaseBranch, dryRun, { gitPush: true }); run(`git switch -c ${releaseBranch}`, repoDir, dryRun); - run(`git merge origin/${devBranch} -m "Merge ${devBranch} into ${branch} for release [skip ci]"`, repoDir, dryRun); + run(`git merge origin/${sourceBranch} -m "Merge ${sourceBranch} into ${branch} for release [skip ci]"`, repoDir, dryRun); run(`git push -u origin ${releaseBranch}`, repoDir, dryRun); maybeCreatePullRequest(repoDir, repo, branch, releaseBranch, dryRun); return { status: dryRun ? 'dry-run' : 'prepared-pr', releaseBranch, - sourceBranch: devBranch, + sourceBranch, targetBranch: branch }; } function ensureMainContainsStableChanges(repoDir, config, branch, dryRun) { if (dryRun) return; - const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; + const sourceBranch = getReleaseSourceBranch(repoDir, {}, config, branch, dryRun); + if (sourceBranch === branch) return; try { - runQuiet(`git fetch origin ${devBranch}:refs/remotes/origin/${devBranch}`, repoDir); - const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${devBranch}`, repoDir)) || 0; + runQuiet(`git fetch origin ${sourceBranch}:refs/remotes/origin/${sourceBranch}`, repoDir); + const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${sourceBranch}`, repoDir)) || 0; if (commitsAhead > 0) { throw new Error( - `${commitsAhead} commit(s) still in ${devBranch} but not in ${branch}. ` + + `${commitsAhead} commit(s) still in ${sourceBranch} but not in ${branch}. ` + 'Merge the release PR into main before running mode=stable-publish.' ); } @@ -317,11 +379,12 @@ function ensureMainContainsStableChanges(repoDir, config, branch, dryRun) { if (err.message.includes('Merge the release PR')) { throw err; } - console.log(`Warning: Could not validate ${devBranch} vs ${branch}: ${err.message}`); + console.log(`Warning: Could not validate ${sourceBranch} vs ${branch}: ${err.message}`); } } -function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun) { +function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = {}) { + const required = toBool(options.required, false); if (dryRun) { console.log(`[dry-run] Would wait for and merge PR: ${headBranch} -> ${baseBranch}`); return { status: 'dry-run', prNumber: null }; @@ -329,6 +392,9 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun) { const slug = parseGitHubRepoSlug(repo.repo || runQuiet('git remote get-url origin', repoDir)); if (!slug) { + if (required) { + throw new Error('Could not determine GitHub repo slug. Cannot merge required release PR.'); + } console.log('Warning: Could not determine GitHub repo slug. Skipping PR merge.'); return { status: 'skip', reason: 'no-slug' }; } @@ -336,6 +402,9 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun) { try { runQuiet('gh --version', repoDir); } catch (err) { + if (required) { + throw new Error('gh CLI not available in runner. Cannot merge required release PR.'); + } console.log('gh CLI not available in runner. Skipping automatic PR merge.'); return { status: 'skip', reason: 'no-gh-cli' }; } @@ -347,11 +416,17 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun) { try { prNumber = parseInt(runQuiet(prQuery, repoDir), 10); } catch (err) { + if (required) { + throw new Error(`No open PR found for ${headBranch} -> ${baseBranch}`); + } console.log(`No open PR found for ${headBranch} -> ${baseBranch}`); return { status: 'skip', reason: 'no-pr' }; } if (!prNumber) { + if (required) { + throw new Error(`No open PR found for ${headBranch} -> ${baseBranch}`); + } console.log(`No open PR found for ${headBranch} -> ${baseBranch}`); return { status: 'skip', reason: 'no-pr' }; } @@ -401,7 +476,7 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun) { } console.log(`Merging PR #${prNumber}...`); - run(`gh pr merge ${prNumber} --repo ${slug} --squash --auto --delete-branch`.trim(), repoDir, dryRun); + run(`gh pr merge ${prNumber} --repo ${slug} --merge --auto --delete-branch`.trim(), repoDir, dryRun); // Fetch the updated main branch run(`git fetch origin ${baseBranch}`, repoDir, dryRun); @@ -726,7 +801,7 @@ function ensureCleanBeforeVersion(repoDir, dryRun, options = {}) { run(`git commit -m "${commitMessage}"`, repoDir, dryRun); } -function publishStable(repoDir, modeConfig, dryRun, buildCmd) { +function bumpStableVersion(repoDir, modeConfig, dryRun) { ensureCleanBeforeVersion(repoDir, dryRun, { autoCommit: modeConfig.autoCommitBeforeVersion ?? (process.env.GITHUB_ACTIONS === 'true'), commitMessage: modeConfig.preVersionCommitMessage || 'chore(release): sync release prep changes [skip ci]' @@ -739,6 +814,15 @@ function publishStable(repoDir, modeConfig, dryRun, buildCmd) { run(`npm version ${bump} -m "Release %s" --ignore-scripts`, repoDir, dryRun); } + const pkg = getPackageJson(repoDir); + return { + packageName: pkg ? pkg.name : null, + version: getPackageVersion(repoDir), + tag: modeConfig.npmTag || 'latest' + }; +} + +function publishPreparedStable(repoDir, modeConfig, dryRun, buildCmd) { const pkg = getPackageJson(repoDir); const packageName = pkg ? pkg.name : null; const version = getPackageVersion(repoDir); @@ -747,11 +831,9 @@ function publishStable(repoDir, modeConfig, dryRun, buildCmd) { ? `--tag ${modeConfig.npmTag}` : ''; ensurePublishableArtifacts(repoDir, dryRun, buildCmd); - // Ignore lifecycle scripts to avoid postpublish git pushes in CI. console.log(`Publishing ${packageName || 'package'}@${version} with tag ${modeConfig.npmTag || 'latest'}...`); run(`npm publish ${tag} --ignore-scripts --no-provenance`.trim(), repoDir, dryRun); - // Wait for the version to be available on npm registry if (!dryRun && packageName && version) { console.log(`Waiting for ${packageName}@${version} to be available on npm...`); const registryReady = waitForNpmVersion(packageName, version, repoDir, 120000, 3000); @@ -768,6 +850,11 @@ function publishStable(repoDir, modeConfig, dryRun, buildCmd) { return { packageName, version, tag: modeConfig.npmTag || 'latest' }; } +function publishStable(repoDir, modeConfig, dryRun, buildCmd) { + bumpStableVersion(repoDir, modeConfig, dryRun); + return publishPreparedStable(repoDir, modeConfig, dryRun, buildCmd); +} + function publishTest(repoDir, modeConfig, dryRun, buildCmd) { const preid = modeConfig.preid || 'test'; const pkg = getPackageJson(repoDir); @@ -1029,6 +1116,7 @@ function main() { const repoDir = path.resolve(configDir, repo.path); const branch = branchOverride || repo.branch || modeConfig.branch || config.defaultBranch || 'main'; const effectiveModeConfig = { ...modeConfig, branch }; + let stablePublishReleaseBranch = null; console.log(`\n==> ${repo.name} (${repoDir})`); @@ -1077,34 +1165,8 @@ function main() { } if (isStablePublishMode) { - // New unified flow: prepare PR, wait for merge, then publish - const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; - - // Step 1: Create release branch and PR - console.log(`Step 1/3: Creating release PR from ${devBranch} -> ${branch}...`); - const prepareResult = prepareStablePullRequest(repoDir, repo, config, branch, dryRun, branchOverride); - - if (prepareResult.status === 'skipped') { - console.log(`Skipping ${repo.name}: ${prepareResult.reason}`); - summary.push({ - name: repo.name, - status: 'skipped', - reason: prepareResult.reason || null - }); - continue; - } - - // Step 2: Wait for PR merge (with auto-merge) - console.log(`Step 2/3: Merging release PR...`); - const mergeResult = waitForPRMerge(repoDir, repo, prepareResult.releaseBranch, branch, dryRun); - - if (mergeResult.status === 'skip') { - console.log(`Warning: PR merge skipped (${mergeResult.reason}). Continuing with publish...`); - } - - // Step 3: Continue with publish (no need for ensureMainContainsStableChanges since we just merged) - console.log(`Step 3/3: Publishing packages...`); - // Fall through to regular publish logic below + console.log(`Preparing protected-branch release from ${branch} for ${repo.name}...`); + stablePublishReleaseBranch = createReleaseBranch(repoDir, repo, branch, dryRun); } // Skip ahead/behind check in dry-run since git commands don't actually execute @@ -1134,23 +1196,23 @@ function main() { if (isStableMode) { const skipIfNoDiff = repo.skipIfNoDiff ?? config.skipIfNoDiff ?? true; const shouldCheckDiff = !dryRun && skipIfNoDiff && !branchOverride; + const sourceBranch = getReleaseSourceBranch(repoDir, repo, config, branch, dryRun); - if (shouldCheckDiff) { - // For stable mode: check if dev branch has changes that main doesn't - const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; + if (shouldCheckDiff && sourceBranch !== branch) { + // For stable mode: check if source branch has changes that target doesn't - // Ensure we have latest dev refs + // Ensure we have latest source refs try { - runQuiet(`git fetch origin ${devBranch}:refs/remotes/origin/${devBranch}`, repoDir); + runQuiet(`git fetch origin ${sourceBranch}:refs/remotes/origin/${sourceBranch}`, repoDir); } catch (err) { - console.log(`Warning: Could not fetch ${devBranch}: ${err.message}`); + console.log(`Warning: Could not fetch ${sourceBranch}: ${err.message}`); } - // Count commits that dev has but main doesn't - const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${devBranch}`, repoDir)) || 0; + // Count commits that source has but target doesn't + const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${sourceBranch}`, repoDir)) || 0; if (commitsAhead === 0) { - console.log(`No changes in origin/${devBranch} vs ${branch}. Skipping publish.`); + console.log(`No changes in origin/${sourceBranch} vs ${branch}. Skipping publish.`); summary.push({ name: repo.name, status: 'skipped', @@ -1158,7 +1220,7 @@ function main() { }); continue; } else { - console.log(`Found ${commitsAhead} commit(s) in ${devBranch} not in ${branch}. Will merge and publish.`); + console.log(`Found ${commitsAhead} commit(s) in ${sourceBranch} not in ${branch}. Will merge and publish.`); shouldMergeDev = true; } } @@ -1166,29 +1228,33 @@ function main() { // For stable mode: check if we need to merge dev (even if skipIfNoDiff is disabled) if (isStableMode && !shouldMergeDev) { - const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; + const sourceBranch = getReleaseSourceBranch(repoDir, repo, config, branch, dryRun); + if (sourceBranch === branch) { + shouldMergeDev = false; + } else { try { - runQuiet(`git fetch origin ${devBranch}:refs/remotes/origin/${devBranch}`, repoDir); - const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${devBranch}`, repoDir)) || 0; + runQuiet(`git fetch origin ${sourceBranch}:refs/remotes/origin/${sourceBranch}`, repoDir); + const commitsAhead = parseInt(runQuiet(`git rev-list --count ${branch}..origin/${sourceBranch}`, repoDir)) || 0; if (commitsAhead > 0) { - console.log(`Found ${commitsAhead} commit(s) in ${devBranch} not in ${branch}. Will merge before publish.`); + console.log(`Found ${commitsAhead} commit(s) in ${sourceBranch} not in ${branch}. Will merge before publish.`); shouldMergeDev = true; } } catch (err) { - console.log(`Warning: Could not check ${devBranch}: ${err.message}`); + console.log(`Warning: Could not check ${sourceBranch}: ${err.message}`); + } } } // Merge dev into main before publishing (stable mode only) if (isStableMode && shouldMergeDev) { - const devBranch = getModeBranch(config, 'test', 'dev') || 'dev'; - console.log(`Merging origin/${devBranch} into ${branch}...`); + const sourceBranch = getReleaseSourceBranch(repoDir, repo, config, branch, dryRun); + console.log(`Merging origin/${sourceBranch} into ${branch}...`); try { - run(`git merge origin/${devBranch} -m "Merge ${devBranch} into ${branch} for release [skip ci]"`, repoDir, dryRun); + run(`git merge origin/${sourceBranch} -m "Merge ${sourceBranch} into ${branch} for release [skip ci]"`, repoDir, dryRun); } catch (err) { - throw new Error(`Failed to merge origin/${devBranch} into ${branch}. Please resolve conflicts manually.`); + throw new Error(`Failed to merge origin/${sourceBranch} into ${branch}. Please resolve conflicts manually.`); } } @@ -1232,7 +1298,29 @@ function main() { tag: result.tag, publishedAs: result.packageName ? `${result.packageName}@${result.version}` : null }); - } else if (isStableMode || isStablePublishMode) { + } else if (isStablePublishMode) { + bumpStableVersion(repoDir, effectiveModeConfig, dryRun); + + console.log(`Step 1/3: Creating release PR from ${stablePublishReleaseBranch} -> ${branch}...`); + run(`git push -u origin ${stablePublishReleaseBranch}`, repoDir, dryRun); + maybeCreatePullRequest(repoDir, repo, branch, stablePublishReleaseBranch, dryRun, { required: true }); + + console.log('Step 2/3: Auto-merging release PR...'); + waitForPRMerge(repoDir, repo, stablePublishReleaseBranch, branch, dryRun, { required: true }); + + ensureBranch(repoDir, branch, dryRun); + + console.log('Step 3/3: Publishing packages from merged branch...'); + const result = publishPreparedStable(repoDir, effectiveModeConfig, dryRun, effectiveBuildCmd); + summary.push({ + name: repo.name, + status: dryRun ? 'dry-run' : 'published', + packageName: result.packageName || null, + version: result.version, + tag: result.tag, + publishedAs: result.packageName ? `${result.packageName}@${result.version}` : null + }); + } else if (isStableMode) { const result = publishStable(repoDir, effectiveModeConfig, dryRun, effectiveBuildCmd); summary.push({ name: repo.name, From 4a3b0f11be35e161a536b6a21d3ef516c126a396 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 00:23:35 +0100 Subject: [PATCH 04/29] preflight token --- .github/workflows/release.yml | 15 ++++++++++++++- scripts/release-orchestrator.js | 12 +++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9f67aa..811fadd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,11 +48,24 @@ jobs: - name: Update npm to latest run: npm install -g npm@latest + - name: Preflight GitHub token permissions + env: + GH_TOKEN: ${{ secrets.GIT_PUSH_TOKEN }} + run: | + if [ -z "$GH_TOKEN" ]; then + echo "GIT_PUSH_TOKEN secret is missing" >&2 + exit 1 + fi + gh --version + gh auth status -h github.com + gh api repos/SolidOS/solid-logic --jq '.full_name + " permissions=" + (if .permissions.push then "push" else "no-push" end)' + - name: Run release orchestrator env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - GITHUB_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ secrets.GIT_PUSH_TOKEN }} GIT_PUSH_TOKEN: ${{ secrets.GIT_PUSH_TOKEN }} + GH_TOKEN: ${{ secrets.GIT_PUSH_TOKEN }} run: | BRANCH_ARG="" if [ -n "${{ inputs.branch }}" ]; then diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 0896373..d0b2251 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -23,6 +23,12 @@ function parseArgs(argv) { return args; } +function preferGhToken() { + if (process.env.GIT_PUSH_TOKEN && !process.env.GH_TOKEN) { + process.env.GH_TOKEN = process.env.GIT_PUSH_TOKEN; + } +} + function toBool(value, defaultValue = false) { if (value === undefined) return defaultValue; if (typeof value === 'boolean') return value; @@ -307,7 +313,10 @@ function maybeCreatePullRequest(repoDir, repo, baseBranch, headBranch, dryRun, o return { status: 'created' }; } catch (err) { if (required) { - throw new Error(`PR create failed for ${repo.name}: ${err.message}`); + throw new Error( + `PR create failed for ${repo.name}: ${err.message}. ` + + 'Ensure GH_TOKEN/GIT_PUSH_TOKEN has access to target repo and Pull requests: Read and write permission.' + ); } console.log(`PR create skipped for ${repo.name}: ${err.message}`); return { status: 'skipped', reason: 'create-failed' }; @@ -1075,6 +1084,7 @@ function publishTest(repoDir, modeConfig, dryRun, buildCmd) { } function main() { + preferGhToken(); const args = parseArgs(process.argv.slice(2)); const mode = args.mode || 'stable'; const configPath = path.resolve(process.cwd(), args.config || 'release.config.json'); From 42e917f82ec329a463b2b450f718786ae41c5dab Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 00:33:07 +0100 Subject: [PATCH 05/29] change merge strategy --- scripts/release-orchestrator.js | 80 ++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index d0b2251..214aa9a 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -23,6 +23,12 @@ function parseArgs(argv) { return args; } +function sleepMs(ms) { + const duration = Math.max(0, Number(ms) || 0); + if (duration === 0) return; + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, duration); +} + function preferGhToken() { if (process.env.GIT_PUSH_TOKEN && !process.env.GH_TOKEN) { process.env.GH_TOKEN = process.env.GIT_PUSH_TOKEN; @@ -440,52 +446,52 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = return { status: 'skip', reason: 'no-pr' }; } - console.log(`Found PR #${prNumber}. Waiting for checks to pass...`); + console.log(`Found PR #${prNumber}. Requesting auto-merge...`); + run(`gh pr merge ${prNumber} --repo ${slug} --merge --auto --delete-branch`.trim(), repoDir, dryRun); - // Wait for checks (with timeout of 10 minutes) - const maxWaitTime = 10 * 60 * 1000; - const checkInterval = 10 * 1000; + // Wait for GitHub to merge the PR after checks/rules are satisfied. + const maxWaitTime = 20 * 60 * 1000; + const pollInterval = 15 * 1000; const startTime = Date.now(); - let checksComplete = false; - while (!checksComplete && (Date.now() - startTime) < maxWaitTime) { + while ((Date.now() - startTime) < maxWaitTime) { + let payload; try { - const statusQuery = `gh pr view ${prNumber} --repo ${slug} --json statusCheckRollup --jq '.statusCheckRollup[0].status // "PENDING"'`; - const status = runQuiet(statusQuery, repoDir).trim(); - console.log(` PR #${prNumber} status: ${status}`); - - if (status === 'SUCCESS') { - checksComplete = true; - } else if (status === 'FAILURE') { - throw new Error(`PR #${prNumber} checks failed`); - } else { - // PENDING or unknown, wait and retry - console.log(` Waiting ${checkInterval / 1000}s before next check...`); - if (!dryRun) { - // Only sleep if not dry-run - const now = Date.now(); - while (Date.now() - now < checkInterval) { - // Busy wait (or use setTimeout in a real implementation) - } - } - } + const query = `gh pr view ${prNumber} --repo ${slug} --json state,mergedAt,mergeStateStatus,statusCheckRollup`; + payload = JSON.parse(runQuiet(query, repoDir) || '{}'); } catch (err) { - console.log(` Warning checking PR status: ${err.message}`); - if (!dryRun) { - const now = Date.now(); - while (Date.now() - now < checkInterval) { - // Busy wait - } - } + console.log(` Warning reading PR state: ${err.message}`); + sleepMs(pollInterval); + continue; } - } - if (!checksComplete) { - console.warn(`Checks did not pass within ${maxWaitTime / 1000 / 60} minutes. Attempting merge anyway...`); + const state = payload.state || 'UNKNOWN'; + const mergedAt = payload.mergedAt || null; + const mergeStateStatus = payload.mergeStateStatus || 'UNKNOWN'; + const checks = Array.isArray(payload.statusCheckRollup) ? payload.statusCheckRollup : []; + const pendingChecks = checks.filter((c) => { + const s = String((c && c.status) || '').toUpperCase(); + return s === 'PENDING' || s === 'IN_PROGRESS' || s === 'QUEUED' || s === 'EXPECTED'; + }).length; + + console.log(` PR #${prNumber}: state=${state}, mergeState=${mergeStateStatus}, pendingChecks=${pendingChecks}`); + + if (mergedAt) { + console.log(`PR #${prNumber} merged at ${mergedAt}.`); + break; + } + + if (state === 'CLOSED' && !mergedAt) { + throw new Error(`PR #${prNumber} was closed without merge.`); + } + + sleepMs(pollInterval); } - console.log(`Merging PR #${prNumber}...`); - run(`gh pr merge ${prNumber} --repo ${slug} --merge --auto --delete-branch`.trim(), repoDir, dryRun); + const mergedAtFinal = runQuiet(`gh pr view ${prNumber} --repo ${slug} --json mergedAt --jq '.mergedAt // ""'`, repoDir).trim(); + if (!mergedAtFinal) { + throw new Error(`Timed out waiting for PR #${prNumber} to merge.`); + } // Fetch the updated main branch run(`git fetch origin ${baseBranch}`, repoDir, dryRun); From b04620ce82a6e75930705a949dafca36215bb25c Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 00:45:33 +0100 Subject: [PATCH 06/29] fix(release): use --admin merge to bypass branch protection review requirement --- RELEASE-STATUS.md | 373 ++++++++++++++++++++++++++++++++ scripts/release-orchestrator.js | 47 +--- 2 files changed, 378 insertions(+), 42 deletions(-) create mode 100644 RELEASE-STATUS.md diff --git a/RELEASE-STATUS.md b/RELEASE-STATUS.md new file mode 100644 index 0000000..e6e9e6f --- /dev/null +++ b/RELEASE-STATUS.md @@ -0,0 +1,373 @@ +# Release Orchestrator - Current Status + +**Date:** March 19, 2026 +**Status:** ⚠ Stable flow semantics clarified (publish-from-main objective documented) + +--- + +## Latest Test Run Results + +``` +Release summary: +- profile-pane: published 3.1.2-test.5 (test) +- solid-panes: published 4.2.3-test.0 (test) +- mashlib: published 2.1.3-test.0 (test) +Summary written to /home/runner/work/solidos/solidos/release-summary.json +``` + +## Authentication Setup (RESOLVED) + +### Individual Repos (SolidOS org) +- **ci.yml workflows** use OIDC authentication +- `permissions: id-token: write` +- No secrets needed +- npm trusted publisher: `SolidOS/solid-panes` → `solid-panes` package +- Works perfectly with npm provenance + +### Central Orchestrator (solidos/solidos repo) +- **release.yml** uses manual token (`NODE_AUTH_TOKEN`) +- Cannot use OIDC due to npm limitation: **one trusted publisher per package** +- Packages already trust their individual repos, can't add second trusted publisher +- Token requires: Granular Access Token with "Bypass 2FA" permission ✅ +- Added `--no-provenance` flag to prevent provenance conflicts + +**Why solid-panes had 403 but profile-pane worked:** +- solid-panes was published with provenance via OIDC before +- npm expected provenance on subsequent publishes +- Manual token can't provide provenance +- Solution: `--no-provenance` flag explicitly opts out + +--- + +## Clarified Objectives (March 19, 2026) + +1. Stable releases should be published from `main` (or an equivalent production branch), not from feature/dev branches. +2. `stable-prepare-pr` should focus on preparing a merge path from development branch to `main` and surfacing merge conflicts early. +3. `stable-publish` should run after `main` is updated, then version and publish from that updated `main` state. +4. Branch override remains available for special cases, but the default and recommended stable target is `main`. + +### ✅ Completed Features + +1. **Mode-specific npm tag injection** + - Test mode: `npm install pkg@test || npm install pkg@latest` + - Stable mode: `npm install pkg@latest` + - Fallback pattern works correctly + +2. **Skip logic** + - Test mode: Always publishes (no skip) + - Stable mode: Compares `origin/dev` vs `main`, skips if identical + - `--branch` parameter overrides skip logic + +3. **Auto-merge for direct stable mode** + - Merges `origin/dev` → `main` before publishing + - Commit message includes `[skip ci]` to prevent triggering individual ci.yml + +4. **Stable publish PR flow (current implementation)** + - `mode=stable-publish` creates a release branch from the selected target branch, bumps version there, opens PR, auto-merges, then publishes from the merged target branch. + - This supports protected branches and guarantees publish happens after target branch is updated. + - Merge mode is `--merge` (not squash) to preserve release/version commit history. + +5. **Version collision handling** + - Test mode: Checks if version exists, auto-bumps up to 5 times + - Stable mode: Standard bump + +6. **Lifecycle scripts disabled** + - `--ignore-scripts` on `npm publish` prevents postpublish git push + - `npm version ... --ignore-scripts` avoids dirty working tree issues + +7. **Enhanced summary output** + - JSON includes: `packageName`, `version`, `tag`, `publishedAs` + - Written to `release-summary.json` + +8. **Branch checkout improvements** + - `git fetch --all` gets remote branches + - `git switch` with fallback to create from origin + +9. **Dry-run improvements** + - Allows untracked files (friendly for local testing) + +10. **Authentication** + - release.yml uses `NODE_AUTH_TOKEN` + - npm updated to latest in workflow + - `--no-provenance` flag added to both publish commands + +11. **CI git auth hardening** + - Uses `GIT_PUSH_TOKEN` (preferred) and falls back to `GITHUB_TOKEN` + - Authenticated clone URLs in runner mode + - Preflight push check for direct stable mode to fail early on permission issues + +--- + +## How Stable Mode Decides Whether to Publish + +For each repo, the orchestrator runs this decision tree: + +``` +git rev-list --count main..origin/dev + │ + ├─ 0 commits ahead ──► SKIP (already up to date) + │ + └─ N commits ahead ──► MERGE + PUBLISH + │ + ├─ git merge origin/dev (local, inside runner clone) + ├─ lock dependency versions in package.json + ├─ git commit [skip ci] + ├─ npm version patch (bumps version + git tag) + ├─ npm publish + ├─ wait for npm registry propagation + └─ git push origin main --follow-tags +``` + +**Override behaviour (direct stable mode):** +- `--branch ` on the CLI → skip the commit-count check entirely; always merge + publish +- `skipIfNoDiff: false` in config → same; always merge + publish +- Dry-run → commit-count check is also skipped (no real git state exists) + +--- + +## Protected Branch Flow (Recommended) + +When branch protection forbids direct push to `main`, the intended workflow is: + +1. `mode=stable-prepare-pr` + - Prepare/validate merge path from `dev` (or configured integration branch) into `main` + - Surface merge conflicts early (without publishing) +2. Merge PR to `main` (manual or auto-merge policy) +3. `mode=stable-publish` + - Version and publish from updated `main` + +This keeps stable publication tied to `main` history and works with "PR required" rulesets. + +Note: An optional enhancement under consideration is auto-merge during `stable-prepare-pr` to turn it into a conflict-check + merge stage before publish. + +--- + +## Open Questions / Pending Decisions + +### 0. gh CLI vs direct git push for main ✅ RESOLVED + +**The problem:** +The orchestrator currently merges `origin/dev` into `main` locally and then pushes: +```bash +git merge origin/dev -m "Merge dev into main for release [skip ci]" +# ... version bump, publish ... +git push origin main --follow-tags +``` + +**This works only if** the runner's token can push directly to `main`. +If any SolidOS repo has a branch protection rule that requires pull requests (no direct push), this will fail with a 403. + +**Two options:** + +**Option A — Direct push (current approach)** +- Works when branch protection allows the token or PAT to bypass. +- Requires `GIT_PUSH_TOKEN` (a PAT with "Allow bypassing branch protection") or repo rules configured to allow the Actions bot. +- Simpler code, no PR trail. + +**Option B — PR-based merge via `gh` CLI** +```bash +gh pr create --base main --head dev --title "Release [skip ci]" --body "Automated stable release" +gh pr merge --merge --auto +``` +- Works even with strict branch protection ("Require a pull request before merging"). +- Creates an audit trail (PR per release). +- Requires `gh` auth in the runner (`gh auth login` or `GH_TOKEN` env var). +- More complex: need to wait for PR to merge before continuing with publish. + +**Outcome:** +- SolidOS repos use branch protection requiring PR before merge. +- Two-step stable flow was implemented (`stable-prepare-pr` + `stable-publish`). +- Direct `stable` mode remains available only for repos/environments that allow direct push. + +--- + +### 1. Release Summary Artifact ⏳ + +**Current:** `release-summary.json` created but deleted after workflow ends + +**Options:** +- **A. Upload as artifact** (recommended) + ```yaml + - name: Upload release summary + uses: actions/upload-artifact@v4 + if: always() + with: + name: release-summary + path: release-summary.json + retention-days: 30 + ``` + Download from Actions tab → Workflow run → Artifacts section (bottom of page) + +- **B. Cat in logs** (simple) + ```yaml + - name: Show release summary + if: always() + run: cat release-summary.json || echo "No summary generated" + ``` + +- **C. Commit back to repo** (complex, probably overkill) + +**Decision needed:** Which approach to implement? + +--- + +### 2. Dependency Updates in Stable Mode ✅ IMPLEMENTED + +**Current behavior:** +- Test mode: Dependencies are not locked in package.json (ephemeral behavior remains) +- Stable mode: Dependencies installed by `afterInstall` are now locked before publish + +**Implemented algorithm (stable mode):** +```javascript +// After running afterInstall in stable mode: +1. Parse afterInstall commands to find installed packages +2. Get actual installed version from node_modules +3. Update package.json dependencies to exact version +4. npm install (to update package-lock.json) +5. git commit with [skip ci] +6. THEN do version bump and publish +``` + +**Example result:** +```json +{ + "version": "4.2.3", + "dependencies": { + "profile-pane": "3.1.2" // Exact version tested + } +} +``` + +**Defaults implemented:** +- Exact pins for reproducibility (no prefix) +- Update `dependencies` and `devDependencies` when keys already exist +- Order A: lock deps and commit first, then version bump and publish + +**Optional config knobs supported:** +- `lockDependencyFields` (array) +- `lockDependencyPrefix` (string) +- `lockInstallCommand` (string) +- `lockCommitMessage` (string) + +--- + +## Configuration Files + +### release.config.json +```json +{ + "modes": [ + { + "name": "test", + "branch": "dev", + "versionBump": "prerelease", + "preid": "test", + "npmTag": "test" + }, + { + "name": "stable", + "branch": "main", + "versionBump": "patch", + "npmTag": "latest" + } + ], + "repos": [ + { + "name": "profile-pane", + "path": "../profile-pane", + "afterInstall": [] + }, + { + "name": "solid-panes", + "path": "../solid-panes", + "afterInstall": ["npm install profile-pane@latest"] + }, + { + "name": "mashlib", + "path": "../mashlib", + "afterInstall": [ + "npm install solid-panes@latest", + "npm install profile-pane@latest" + ] + } + ] +} +``` + +### Key files modified: +- `scripts/release-orchestrator.js` - Core logic +- `.github/workflows/release.yml` - GitHub Actions workflow +- `RELEASE-HOWTO.md` - Documentation + +--- + +## Known Issues / Limitations + +1. **Git credentials in CI:** Currently relies on GITHUB_TOKEN for git operations +2. **npm token permissions:** Requires "Bypass 2FA" for publishing +3. **Single trusted publisher limit:** Cannot use OIDC for central orchestrator +4. **Dependencies not locked:** Resolved for stable mode in orchestrator implementation + +--- + +## Testing Checklist + +After implementing dependency locking: + +- [ ] Test mode publishes correctly with @test tag +- [ ] Test mode behavior remains as expected for prerelease publishes +- [ ] Stable mode auto-merges dev → main +- [ ] Stable mode updates dependencies to exact versions +- [ ] Stable mode commits with [skip ci] +- [ ] Stable mode publishes with locked dependencies +- [ ] Verify published packages have correct dependency versions on npm +- [ ] Test fallback pattern: `npm install pkg@test || npm install pkg@latest` + +--- + +## Questions for Tomorrow + +1. **Artifact upload:** Which option for release-summary.json? +2. **Dependency locking:** Exact pins or caret ranges? +3. **Which dependency fields:** dependencies only, or also devDependencies? +4. **Verification:** Should we add `npm ls` check after afterInstall to log what was installed? + +--- + +## Quick Reference + +### Run test publish: +```bash +# From solidos repo +node scripts/release-orchestrator.js \ + --config release.config.json \ + --mode test \ + --dry-run=false \ + --clone-missing=true +``` + +### Run stable publish: +```bash +node scripts/release-orchestrator.js \ + --config release.config.json \ + --mode stable \ + --dry-run=false \ + --clone-missing=true +``` + +### Check published versions: +```bash +npm view solid-panes@test +npm view profile-pane@test +npm view mashlib@test +npm dist-tag ls solid-panes +``` + +--- + +## Contact / References + +- Main repo: https://github.com/solidos/solidos +- Individual repos: SolidOS org +- Documentation: RELEASE-HOWTO.md +- This status: RELEASE-STATUS.md diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 214aa9a..0857fbb 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -446,52 +446,15 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = return { status: 'skip', reason: 'no-pr' }; } - console.log(`Found PR #${prNumber}. Requesting auto-merge...`); - run(`gh pr merge ${prNumber} --repo ${slug} --merge --auto --delete-branch`.trim(), repoDir, dryRun); - - // Wait for GitHub to merge the PR after checks/rules are satisfied. - const maxWaitTime = 20 * 60 * 1000; - const pollInterval = 15 * 1000; - const startTime = Date.now(); - - while ((Date.now() - startTime) < maxWaitTime) { - let payload; - try { - const query = `gh pr view ${prNumber} --repo ${slug} --json state,mergedAt,mergeStateStatus,statusCheckRollup`; - payload = JSON.parse(runQuiet(query, repoDir) || '{}'); - } catch (err) { - console.log(` Warning reading PR state: ${err.message}`); - sleepMs(pollInterval); - continue; - } - - const state = payload.state || 'UNKNOWN'; - const mergedAt = payload.mergedAt || null; - const mergeStateStatus = payload.mergeStateStatus || 'UNKNOWN'; - const checks = Array.isArray(payload.statusCheckRollup) ? payload.statusCheckRollup : []; - const pendingChecks = checks.filter((c) => { - const s = String((c && c.status) || '').toUpperCase(); - return s === 'PENDING' || s === 'IN_PROGRESS' || s === 'QUEUED' || s === 'EXPECTED'; - }).length; - - console.log(` PR #${prNumber}: state=${state}, mergeState=${mergeStateStatus}, pendingChecks=${pendingChecks}`); - - if (mergedAt) { - console.log(`PR #${prNumber} merged at ${mergedAt}.`); - break; - } - - if (state === 'CLOSED' && !mergedAt) { - throw new Error(`PR #${prNumber} was closed without merge.`); - } - - sleepMs(pollInterval); - } + console.log(`Found PR #${prNumber}. Merging with admin override...`); + run(`gh pr merge ${prNumber} --repo ${slug} --merge --admin --delete-branch`.trim(), repoDir, dryRun); + // --admin merges immediately; verify it succeeded const mergedAtFinal = runQuiet(`gh pr view ${prNumber} --repo ${slug} --json mergedAt --jq '.mergedAt // ""'`, repoDir).trim(); if (!mergedAtFinal) { - throw new Error(`Timed out waiting for PR #${prNumber} to merge.`); + throw new Error(`PR #${prNumber} was not merged. Check branch protection settings or token admin rights.`); } + console.log(`PR #${prNumber} merged at ${mergedAtFinal}.`); // Fetch the updated main branch run(`git fetch origin ${baseBranch}`, repoDir, dryRun); From 08bc8feb0fe129eb284a820f256bb85d04c6be89 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 01:09:38 +0100 Subject: [PATCH 07/29] test --- RELEASE-STATUS.md | 746 +++++++++++++++++++++++----------------------- 1 file changed, 373 insertions(+), 373 deletions(-) diff --git a/RELEASE-STATUS.md b/RELEASE-STATUS.md index e6e9e6f..a0de1fd 100644 --- a/RELEASE-STATUS.md +++ b/RELEASE-STATUS.md @@ -1,373 +1,373 @@ -# Release Orchestrator - Current Status - -**Date:** March 19, 2026 -**Status:** ⚠ Stable flow semantics clarified (publish-from-main objective documented) - ---- - -## Latest Test Run Results - -``` -Release summary: -- profile-pane: published 3.1.2-test.5 (test) -- solid-panes: published 4.2.3-test.0 (test) -- mashlib: published 2.1.3-test.0 (test) -Summary written to /home/runner/work/solidos/solidos/release-summary.json -``` - -## Authentication Setup (RESOLVED) - -### Individual Repos (SolidOS org) -- **ci.yml workflows** use OIDC authentication -- `permissions: id-token: write` -- No secrets needed -- npm trusted publisher: `SolidOS/solid-panes` → `solid-panes` package -- Works perfectly with npm provenance - -### Central Orchestrator (solidos/solidos repo) -- **release.yml** uses manual token (`NODE_AUTH_TOKEN`) -- Cannot use OIDC due to npm limitation: **one trusted publisher per package** -- Packages already trust their individual repos, can't add second trusted publisher -- Token requires: Granular Access Token with "Bypass 2FA" permission ✅ -- Added `--no-provenance` flag to prevent provenance conflicts - -**Why solid-panes had 403 but profile-pane worked:** -- solid-panes was published with provenance via OIDC before -- npm expected provenance on subsequent publishes -- Manual token can't provide provenance -- Solution: `--no-provenance` flag explicitly opts out - ---- - -## Clarified Objectives (March 19, 2026) - -1. Stable releases should be published from `main` (or an equivalent production branch), not from feature/dev branches. -2. `stable-prepare-pr` should focus on preparing a merge path from development branch to `main` and surfacing merge conflicts early. -3. `stable-publish` should run after `main` is updated, then version and publish from that updated `main` state. -4. Branch override remains available for special cases, but the default and recommended stable target is `main`. - -### ✅ Completed Features - -1. **Mode-specific npm tag injection** - - Test mode: `npm install pkg@test || npm install pkg@latest` - - Stable mode: `npm install pkg@latest` - - Fallback pattern works correctly - -2. **Skip logic** - - Test mode: Always publishes (no skip) - - Stable mode: Compares `origin/dev` vs `main`, skips if identical - - `--branch` parameter overrides skip logic - -3. **Auto-merge for direct stable mode** - - Merges `origin/dev` → `main` before publishing - - Commit message includes `[skip ci]` to prevent triggering individual ci.yml - -4. **Stable publish PR flow (current implementation)** - - `mode=stable-publish` creates a release branch from the selected target branch, bumps version there, opens PR, auto-merges, then publishes from the merged target branch. - - This supports protected branches and guarantees publish happens after target branch is updated. - - Merge mode is `--merge` (not squash) to preserve release/version commit history. - -5. **Version collision handling** - - Test mode: Checks if version exists, auto-bumps up to 5 times - - Stable mode: Standard bump - -6. **Lifecycle scripts disabled** - - `--ignore-scripts` on `npm publish` prevents postpublish git push - - `npm version ... --ignore-scripts` avoids dirty working tree issues - -7. **Enhanced summary output** - - JSON includes: `packageName`, `version`, `tag`, `publishedAs` - - Written to `release-summary.json` - -8. **Branch checkout improvements** - - `git fetch --all` gets remote branches - - `git switch` with fallback to create from origin - -9. **Dry-run improvements** - - Allows untracked files (friendly for local testing) - -10. **Authentication** - - release.yml uses `NODE_AUTH_TOKEN` - - npm updated to latest in workflow - - `--no-provenance` flag added to both publish commands - -11. **CI git auth hardening** - - Uses `GIT_PUSH_TOKEN` (preferred) and falls back to `GITHUB_TOKEN` - - Authenticated clone URLs in runner mode - - Preflight push check for direct stable mode to fail early on permission issues - ---- - -## How Stable Mode Decides Whether to Publish - -For each repo, the orchestrator runs this decision tree: - -``` -git rev-list --count main..origin/dev - │ - ├─ 0 commits ahead ──► SKIP (already up to date) - │ - └─ N commits ahead ──► MERGE + PUBLISH - │ - ├─ git merge origin/dev (local, inside runner clone) - ├─ lock dependency versions in package.json - ├─ git commit [skip ci] - ├─ npm version patch (bumps version + git tag) - ├─ npm publish - ├─ wait for npm registry propagation - └─ git push origin main --follow-tags -``` - -**Override behaviour (direct stable mode):** -- `--branch ` on the CLI → skip the commit-count check entirely; always merge + publish -- `skipIfNoDiff: false` in config → same; always merge + publish -- Dry-run → commit-count check is also skipped (no real git state exists) - ---- - -## Protected Branch Flow (Recommended) - -When branch protection forbids direct push to `main`, the intended workflow is: - -1. `mode=stable-prepare-pr` - - Prepare/validate merge path from `dev` (or configured integration branch) into `main` - - Surface merge conflicts early (without publishing) -2. Merge PR to `main` (manual or auto-merge policy) -3. `mode=stable-publish` - - Version and publish from updated `main` - -This keeps stable publication tied to `main` history and works with "PR required" rulesets. - -Note: An optional enhancement under consideration is auto-merge during `stable-prepare-pr` to turn it into a conflict-check + merge stage before publish. - ---- - -## Open Questions / Pending Decisions - -### 0. gh CLI vs direct git push for main ✅ RESOLVED - -**The problem:** -The orchestrator currently merges `origin/dev` into `main` locally and then pushes: -```bash -git merge origin/dev -m "Merge dev into main for release [skip ci]" -# ... version bump, publish ... -git push origin main --follow-tags -``` - -**This works only if** the runner's token can push directly to `main`. -If any SolidOS repo has a branch protection rule that requires pull requests (no direct push), this will fail with a 403. - -**Two options:** - -**Option A — Direct push (current approach)** -- Works when branch protection allows the token or PAT to bypass. -- Requires `GIT_PUSH_TOKEN` (a PAT with "Allow bypassing branch protection") or repo rules configured to allow the Actions bot. -- Simpler code, no PR trail. - -**Option B — PR-based merge via `gh` CLI** -```bash -gh pr create --base main --head dev --title "Release [skip ci]" --body "Automated stable release" -gh pr merge --merge --auto -``` -- Works even with strict branch protection ("Require a pull request before merging"). -- Creates an audit trail (PR per release). -- Requires `gh` auth in the runner (`gh auth login` or `GH_TOKEN` env var). -- More complex: need to wait for PR to merge before continuing with publish. - -**Outcome:** -- SolidOS repos use branch protection requiring PR before merge. -- Two-step stable flow was implemented (`stable-prepare-pr` + `stable-publish`). -- Direct `stable` mode remains available only for repos/environments that allow direct push. - ---- - -### 1. Release Summary Artifact ⏳ - -**Current:** `release-summary.json` created but deleted after workflow ends - -**Options:** -- **A. Upload as artifact** (recommended) - ```yaml - - name: Upload release summary - uses: actions/upload-artifact@v4 - if: always() - with: - name: release-summary - path: release-summary.json - retention-days: 30 - ``` - Download from Actions tab → Workflow run → Artifacts section (bottom of page) - -- **B. Cat in logs** (simple) - ```yaml - - name: Show release summary - if: always() - run: cat release-summary.json || echo "No summary generated" - ``` - -- **C. Commit back to repo** (complex, probably overkill) - -**Decision needed:** Which approach to implement? - ---- - -### 2. Dependency Updates in Stable Mode ✅ IMPLEMENTED - -**Current behavior:** -- Test mode: Dependencies are not locked in package.json (ephemeral behavior remains) -- Stable mode: Dependencies installed by `afterInstall` are now locked before publish - -**Implemented algorithm (stable mode):** -```javascript -// After running afterInstall in stable mode: -1. Parse afterInstall commands to find installed packages -2. Get actual installed version from node_modules -3. Update package.json dependencies to exact version -4. npm install (to update package-lock.json) -5. git commit with [skip ci] -6. THEN do version bump and publish -``` - -**Example result:** -```json -{ - "version": "4.2.3", - "dependencies": { - "profile-pane": "3.1.2" // Exact version tested - } -} -``` - -**Defaults implemented:** -- Exact pins for reproducibility (no prefix) -- Update `dependencies` and `devDependencies` when keys already exist -- Order A: lock deps and commit first, then version bump and publish - -**Optional config knobs supported:** -- `lockDependencyFields` (array) -- `lockDependencyPrefix` (string) -- `lockInstallCommand` (string) -- `lockCommitMessage` (string) - ---- - -## Configuration Files - -### release.config.json -```json -{ - "modes": [ - { - "name": "test", - "branch": "dev", - "versionBump": "prerelease", - "preid": "test", - "npmTag": "test" - }, - { - "name": "stable", - "branch": "main", - "versionBump": "patch", - "npmTag": "latest" - } - ], - "repos": [ - { - "name": "profile-pane", - "path": "../profile-pane", - "afterInstall": [] - }, - { - "name": "solid-panes", - "path": "../solid-panes", - "afterInstall": ["npm install profile-pane@latest"] - }, - { - "name": "mashlib", - "path": "../mashlib", - "afterInstall": [ - "npm install solid-panes@latest", - "npm install profile-pane@latest" - ] - } - ] -} -``` - -### Key files modified: -- `scripts/release-orchestrator.js` - Core logic -- `.github/workflows/release.yml` - GitHub Actions workflow -- `RELEASE-HOWTO.md` - Documentation - ---- - -## Known Issues / Limitations - -1. **Git credentials in CI:** Currently relies on GITHUB_TOKEN for git operations -2. **npm token permissions:** Requires "Bypass 2FA" for publishing -3. **Single trusted publisher limit:** Cannot use OIDC for central orchestrator -4. **Dependencies not locked:** Resolved for stable mode in orchestrator implementation - ---- - -## Testing Checklist - -After implementing dependency locking: - -- [ ] Test mode publishes correctly with @test tag -- [ ] Test mode behavior remains as expected for prerelease publishes -- [ ] Stable mode auto-merges dev → main -- [ ] Stable mode updates dependencies to exact versions -- [ ] Stable mode commits with [skip ci] -- [ ] Stable mode publishes with locked dependencies -- [ ] Verify published packages have correct dependency versions on npm -- [ ] Test fallback pattern: `npm install pkg@test || npm install pkg@latest` - ---- - -## Questions for Tomorrow - -1. **Artifact upload:** Which option for release-summary.json? -2. **Dependency locking:** Exact pins or caret ranges? -3. **Which dependency fields:** dependencies only, or also devDependencies? -4. **Verification:** Should we add `npm ls` check after afterInstall to log what was installed? - ---- - -## Quick Reference - -### Run test publish: -```bash -# From solidos repo -node scripts/release-orchestrator.js \ - --config release.config.json \ - --mode test \ - --dry-run=false \ - --clone-missing=true -``` - -### Run stable publish: -```bash -node scripts/release-orchestrator.js \ - --config release.config.json \ - --mode stable \ - --dry-run=false \ - --clone-missing=true -``` - -### Check published versions: -```bash -npm view solid-panes@test -npm view profile-pane@test -npm view mashlib@test -npm dist-tag ls solid-panes -``` - ---- - -## Contact / References - -- Main repo: https://github.com/solidos/solidos -- Individual repos: SolidOS org -- Documentation: RELEASE-HOWTO.md -- This status: RELEASE-STATUS.md +# Release Orchestrator - Current Status + +**Date:** March 19, 2026 +**Status:** ⚠ Stable flow semantics clarified (publish-from-main objective documented) + +--- + +## Latest Test Run Results + +``` +Release summary: +- profile-pane: published 3.1.2-test.5 (test) +- solid-panes: published 4.2.3-test.0 (test) +- mashlib: published 2.1.3-test.0 (test) +Summary written to /home/runner/work/solidos/solidos/release-summary.json +``` + +## Authentication Setup (RESOLVED) + +### Individual Repos (SolidOS org) +- **ci.yml workflows** use OIDC authentication +- `permissions: id-token: write` +- No secrets needed +- npm trusted publisher: `SolidOS/solid-panes` → `solid-panes` package +- Works perfectly with npm provenance + +### Central Orchestrator (solidos/solidos repo) +- **release.yml** uses manual token (`NODE_AUTH_TOKEN`) +- Cannot use OIDC due to npm limitation: **one trusted publisher per package** +- Packages already trust their individual repos, can't add second trusted publisher +- Token requires: Granular Access Token with "Bypass 2FA" permission ✅ +- Added `--no-provenance` flag to prevent provenance conflicts + +**Why solid-panes had 403 but profile-pane worked:** +- solid-panes was published with provenance via OIDC before +- npm expected provenance on subsequent publishes +- Manual token can't provide provenance +- Solution: `--no-provenance` flag explicitly opts out + +--- + +## Clarified Objectives (March 19, 2026) + +1. Stable releases should be published from `main` (or an equivalent production branch), not from feature/dev branches. +2. `stable-prepare-pr` should focus on preparing a merge path from development branch to `main` and surfacing merge conflicts early. +3. `stable-publish` should run after `main` is updated, then version and publish from that updated `main` state. +4. Branch override remains available for special cases, but the default and recommended stable target is `main`. + +### ✅ Completed Features + +1. **Mode-specific npm tag injection** + - Test mode: `npm install pkg@test || npm install pkg@latest` + - Stable mode: `npm install pkg@latest` + - Fallback pattern works correctly + +2. **Skip logic** + - Test mode: Always publishes (no skip) + - Stable mode: Compares `origin/dev` vs `main`, skips if identical + - `--branch` parameter overrides skip logic + +3. **Auto-merge for direct stable mode** + - Merges `origin/dev` → `main` before publishing + - Commit message includes `[skip ci]` to prevent triggering individual ci.yml + +4. **Stable publish PR flow (current implementation)** + - `mode=stable-publish` creates a release branch from the selected target branch, bumps version there, opens PR, auto-merges, then publishes from the merged target branch. + - This supports protected branches and guarantees publish happens after target branch is updated. + - Merge mode is `--merge` (not squash) to preserve release/version commit history. + +5. **Version collision handling** + - Test mode: Checks if version exists, auto-bumps up to 5 times + - Stable mode: Standard bump + +6. **Lifecycle scripts disabled** + - `--ignore-scripts` on `npm publish` prevents postpublish git push + - `npm version ... --ignore-scripts` avoids dirty working tree issues + +7. **Enhanced summary output** + - JSON includes: `packageName`, `version`, `tag`, `publishedAs` + - Written to `release-summary.json` + +8. **Branch checkout improvements** + - `git fetch --all` gets remote branches + - `git switch` with fallback to create from origin + +9. **Dry-run improvements** + - Allows untracked files (friendly for local testing) + +10. **Authentication** + - release.yml uses `NODE_AUTH_TOKEN` + - npm updated to latest in workflow + - `--no-provenance` flag added to both publish commands + +11. **CI git auth hardening** + - Uses `GIT_PUSH_TOKEN` (preferred) and falls back to `GITHUB_TOKEN` + - Authenticated clone URLs in runner mode + - Preflight push check for direct stable mode to fail early on permission issues + +--- + +## How Stable Mode Decides Whether to Publish + +For each repo, the orchestrator runs this decision tree: + +``` +git rev-list --count main..origin/dev + │ + ├─ 0 commits ahead ──► SKIP (already up to date) + │ + └─ N commits ahead ──► MERGE + PUBLISH + │ + ├─ git merge origin/dev (local, inside runner clone) + ├─ lock dependency versions in package.json + ├─ git commit [skip ci] + ├─ npm version patch (bumps version + git tag) + ├─ npm publish + ├─ wait for npm registry propagation + └─ git push origin main --follow-tags +``` + +**Override behaviour (direct stable mode):** +- `--branch ` on the CLI → skip the commit-count check entirely; always merge + publish +- `skipIfNoDiff: false` in config → same; always merge + publish +- Dry-run → commit-count check is also skipped (no real git state exists) + +--- + +## Protected Branch Flow (Recommended) + +When branch protection forbids direct push to `main`, the intended workflow is: + +1. `mode=stable-prepare-pr` + - Prepare/validate merge path from `dev` (or configured integration branch) into `main` + - Surface merge conflicts early (without publishing) +2. Merge PR to `main` (manual or auto-merge policy) +3. `mode=stable-publish` + - Version and publish from updated `main` + +This keeps stable publication tied to `main` history and works with "PR required" rulesets. + +Note: An optional enhancement under consideration is auto-merge during `stable-prepare-pr` to turn it into a conflict-check + merge stage before publish. + +--- + +## Open Questions / Pending Decisions + +### 0. gh CLI vs direct git push for main ✅ RESOLVED + +**The problem:** +The orchestrator currently merges `origin/dev` into `main` locally and then pushes: +```bash +git merge origin/dev -m "Merge dev into main for release [skip ci]" +# ... version bump, publish ... +git push origin main --follow-tags +``` + +**This works only if** the runner's token can push directly to `main`. +If any SolidOS repo has a branch protection rule that requires pull requests (no direct push), this will fail with a 403. + +**Two options:** + +**Option A — Direct push (current approach)** +- Works when branch protection allows the token or PAT to bypass. +- Requires `GIT_PUSH_TOKEN` (a PAT with "Allow bypassing branch protection") or repo rules configured to allow the Actions bot. +- Simpler code, no PR trail. + +**Option B — PR-based merge via `gh` CLI** +```bash +gh pr create --base main --head dev --title "Release [skip ci]" --body "Automated stable release" +gh pr merge --merge --auto +``` +- Works even with strict branch protection ("Require a pull request before merging"). +- Creates an audit trail (PR per release). +- Requires `gh` auth in the runner (`gh auth login` or `GH_TOKEN` env var). +- More complex: need to wait for PR to merge before continuing with publish. + +**Outcome:** +- SolidOS repos use branch protection requiring PR before merge. +- Two-step stable flow was implemented (`stable-prepare-pr` + `stable-publish`). +- Direct `stable` mode remains available only for repos/environments that allow direct push. + +--- + +### 1. Release Summary Artifact ⏳ + +**Current:** `release-summary.json` created but deleted after workflow ends + +**Options:** +- **A. Upload as artifact** (recommended) + ```yaml + - name: Upload release summary + uses: actions/upload-artifact@v4 + if: always() + with: + name: release-summary + path: release-summary.json + retention-days: 30 + ``` + Download from Actions tab → Workflow run → Artifacts section (bottom of page) + +- **B. Cat in logs** (simple) + ```yaml + - name: Show release summary + if: always() + run: cat release-summary.json || echo "No summary generated" + ``` + +- **C. Commit back to repo** (complex, probably overkill) + +**Decision needed:** Which approach to implement? + +--- + +### 2. Dependency Updates in Stable Mode ✅ IMPLEMENTED + +**Current behavior:** +- Test mode: Dependencies are not locked in package.json (ephemeral behavior remains) +- Stable mode: Dependencies installed by `afterInstall` are now locked before publish + +**Implemented algorithm (stable mode):** +```javascript +// After running afterInstall in stable mode: +1. Parse afterInstall commands to find installed packages +2. Get actual installed version from node_modules +3. Update package.json dependencies to exact version +4. npm install (to update package-lock.json) +5. git commit with [skip ci] +6. THEN do version bump and publish +``` + +**Example result:** +```json +{ + "version": "4.2.3", + "dependencies": { + "profile-pane": "3.1.2" // Exact version tested + } +} +``` + +**Defaults implemented:** +- Exact pins for reproducibility (no prefix) +- Update `dependencies` and `devDependencies` when keys already exist +- Order A: lock deps and commit first, then version bump and publish + +**Optional config knobs supported:** +- `lockDependencyFields` (array) +- `lockDependencyPrefix` (string) +- `lockInstallCommand` (string) +- `lockCommitMessage` (string) + +--- + +## Configuration Files + +### release.config.json +```json +{ + "modes": [ + { + "name": "test", + "branch": "dev", + "versionBump": "prerelease", + "preid": "test", + "npmTag": "test" + }, + { + "name": "stable", + "branch": "main", + "versionBump": "patch", + "npmTag": "latest" + } + ], + "repos": [ + { + "name": "profile-pane", + "path": "../profile-pane", + "afterInstall": [] + }, + { + "name": "solid-panes", + "path": "../solid-panes", + "afterInstall": ["npm install profile-pane@latest"] + }, + { + "name": "mashlib", + "path": "../mashlib", + "afterInstall": [ + "npm install solid-panes@latest", + "npm install profile-pane@latest" + ] + } + ] +} +``` + +### Key files modified: +- `scripts/release-orchestrator.js` - Core logic +- `.github/workflows/release.yml` - GitHub Actions workflow +- `RELEASE-HOWTO.md` - Documentation + +--- + +## Known Issues / Limitations + +1. **Git credentials in CI:** Currently relies on GITHUB_TOKEN for git operations +2. **npm token permissions:** Requires "Bypass 2FA" for publishing +3. **Single trusted publisher limit:** Cannot use OIDC for central orchestrator +4. **Dependencies not locked:** Resolved for stable mode in orchestrator implementation + +--- + +## Testing Checklist + +After implementing dependency locking: + +- [ ] Test mode publishes correctly with @test tag +- [ ] Test mode behavior remains as expected for prerelease publishes +- [ ] Stable mode auto-merges dev → main +- [ ] Stable mode updates dependencies to exact versions +- [ ] Stable mode commits with [skip ci] +- [ ] Stable mode publishes with locked dependencies +- [ ] Verify published packages have correct dependency versions on npm +- [ ] Test fallback pattern: `npm install pkg@test || npm install pkg@latest` + +--- + +## Questions for Tomorrow + +1. **Artifact upload:** Which option for release-summary.json? +2. **Dependency locking:** Exact pins or caret ranges? +3. **Which dependency fields:** dependencies only, or also devDependencies? +4. **Verification:** Should we add `npm ls` check after afterInstall to log what was installed? + +--- + +## Quick Reference + +### Run test publish: +```bash +# From solidos repo +node scripts/release-orchestrator.js \ + --config release.config.json \ + --mode test \ + --dry-run=false \ + --clone-missing=true +``` + +### Run stable publish: +```bash +node scripts/release-orchestrator.js \ + --config release.config.json \ + --mode stable \ + --dry-run=false \ + --clone-missing=true +``` + +### Check published versions: +```bash +npm view solid-panes@test +npm view profile-pane@test +npm view mashlib@test +npm dist-tag ls solid-panes +``` + +--- + +## Contact / References + +- Main repo: https://github.com/solidos/solidos +- Individual repos: SolidOS org +- Documentation: RELEASE-HOWTO.md +- This status: RELEASE-STATUS.md From db87fdaef6c657a04bbaff54e638e3e27ea5289f Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 01:19:37 +0100 Subject: [PATCH 08/29] fix(release): handle expected checks before PR merge --- scripts/release-orchestrator.js | 108 +++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 7 deletions(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 0857fbb..e031d3c 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -446,15 +446,109 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = return { status: 'skip', reason: 'no-pr' }; } - console.log(`Found PR #${prNumber}. Merging with admin override...`); - run(`gh pr merge ${prNumber} --repo ${slug} --merge --admin --delete-branch`.trim(), repoDir, dryRun); + console.log(`Found PR #${prNumber}. Requesting auto-merge...`); + let autoMergeRequested = false; + try { + run(`gh pr merge ${prNumber} --repo ${slug} --merge --auto --delete-branch`.trim(), repoDir, dryRun); + autoMergeRequested = true; + } catch (err) { + const message = String(err.message || err); + // Some repos reject auto-merge requests while checks are still being initialized. + if ( + /Repository rule violations found/i.test(message) || + /required status checks are expected/i.test(message) || + /required status check/i.test(message) + ) { + console.log(` Auto-merge not accepted yet: ${message.split('\n')[0]}`); + } else { + throw err; + } + } + + const maxWaitTime = 20 * 60 * 1000; + const pollInterval = 15 * 1000; + const startTime = Date.now(); + let finalMergedAt = ''; + + while ((Date.now() - startTime) < maxWaitTime) { + let payload; + try { + const query = `gh pr view ${prNumber} --repo ${slug} --json state,mergedAt,mergeStateStatus,reviewDecision,statusCheckRollup`; + payload = JSON.parse(runQuiet(query, repoDir) || '{}'); + } catch (err) { + console.log(` Warning reading PR state: ${err.message}`); + sleepMs(pollInterval); + continue; + } + + const state = payload.state || 'UNKNOWN'; + const mergedAt = payload.mergedAt || ''; + const mergeStateStatus = payload.mergeStateStatus || 'UNKNOWN'; + const reviewDecision = payload.reviewDecision || 'UNKNOWN'; + const checks = Array.isArray(payload.statusCheckRollup) ? payload.statusCheckRollup : []; + + const pendingChecks = checks.filter((c) => { + const s = String((c && c.status) || '').toUpperCase(); + return s === 'PENDING' || s === 'IN_PROGRESS' || s === 'QUEUED' || s === 'EXPECTED'; + }).length; + + const failingChecks = checks.filter((c) => { + const conclusion = String((c && c.conclusion) || '').toUpperCase(); + return conclusion === 'FAILURE' || conclusion === 'TIMED_OUT' || conclusion === 'CANCELLED' || conclusion === 'ACTION_REQUIRED'; + }).length; + + console.log( + ` PR #${prNumber}: state=${state}, mergeState=${mergeStateStatus}, review=${reviewDecision}, pendingChecks=${pendingChecks}, failingChecks=${failingChecks}` + ); + + if (mergedAt) { + finalMergedAt = mergedAt; + break; + } + + if (state === 'CLOSED') { + throw new Error(`PR #${prNumber} was closed without merge.`); + } + + if (failingChecks > 0) { + throw new Error(`PR #${prNumber} has failing required checks.`); + } + + // Retry requesting auto-merge once checks have started appearing. + if (!autoMergeRequested && pendingChecks > 0) { + try { + run(`gh pr merge ${prNumber} --repo ${slug} --merge --auto --delete-branch`.trim(), repoDir, dryRun); + autoMergeRequested = true; + console.log(` Auto-merge request accepted for PR #${prNumber}.`); + } catch (err) { + console.log(` Auto-merge still blocked: ${String(err.message || err).split('\n')[0]}`); + } + } - // --admin merges immediately; verify it succeeded - const mergedAtFinal = runQuiet(`gh pr view ${prNumber} --repo ${slug} --json mergedAt --jq '.mergedAt // ""'`, repoDir).trim(); - if (!mergedAtFinal) { - throw new Error(`PR #${prNumber} was not merged. Check branch protection settings or token admin rights.`); + // If everything is green but merge still blocked (e.g. review requirement), try admin merge. + if (pendingChecks === 0 && mergeStateStatus === 'BLOCKED') { + try { + run(`gh pr merge ${prNumber} --repo ${slug} --merge --admin --delete-branch`.trim(), repoDir, dryRun); + } catch (err) { + const message = String(err.message || err); + if (!/required status checks are expected/i.test(message) && !/Repository rule violations found/i.test(message)) { + throw err; + } + } + } + + sleepMs(pollInterval); } - console.log(`PR #${prNumber} merged at ${mergedAtFinal}.`); + + if (!finalMergedAt) { + finalMergedAt = runQuiet(`gh pr view ${prNumber} --repo ${slug} --json mergedAt --jq '.mergedAt // ""'`, repoDir).trim(); + } + + if (!finalMergedAt) { + throw new Error(`Timed out waiting for PR #${prNumber} to merge.`); + } + + console.log(`PR #${prNumber} merged at ${finalMergedAt}.`); // Fetch the updated main branch run(`git fetch origin ${baseBranch}`, repoDir, dryRun); From 9dcd80362db198d5bfb7ec189eda985e857aa2ce Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 01:29:00 +0100 Subject: [PATCH 09/29] fix(release): avoid premature admin merge before checks appear --- scripts/release-orchestrator.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index e031d3c..2660b0c 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -469,6 +469,20 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = const pollInterval = 15 * 1000; const startTime = Date.now(); let finalMergedAt = ''; + let sawAnyChecks = false; + + const isTransientRuleViolation = (err) => { + const text = [ + String(err && err.message ? err.message : ''), + String(err && err.stderr ? err.stderr : ''), + String(err && err.stdout ? err.stdout : '') + ].join('\n'); + return ( + /Repository rule violations found/i.test(text) || + /required status checks are expected/i.test(text) || + /required status check/i.test(text) + ); + }; while ((Date.now() - startTime) < maxWaitTime) { let payload; @@ -486,6 +500,9 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = const mergeStateStatus = payload.mergeStateStatus || 'UNKNOWN'; const reviewDecision = payload.reviewDecision || 'UNKNOWN'; const checks = Array.isArray(payload.statusCheckRollup) ? payload.statusCheckRollup : []; + if (checks.length > 0) { + sawAnyChecks = true; + } const pendingChecks = checks.filter((c) => { const s = String((c && c.status) || '').toUpperCase(); @@ -515,25 +532,26 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = } // Retry requesting auto-merge once checks have started appearing. - if (!autoMergeRequested && pendingChecks > 0) { + if (!autoMergeRequested && (pendingChecks > 0 || sawAnyChecks)) { try { run(`gh pr merge ${prNumber} --repo ${slug} --merge --auto --delete-branch`.trim(), repoDir, dryRun); autoMergeRequested = true; console.log(` Auto-merge request accepted for PR #${prNumber}.`); } catch (err) { - console.log(` Auto-merge still blocked: ${String(err.message || err).split('\n')[0]}`); + console.log(` Auto-merge still blocked: ${String((err && err.message) || err).split('\n')[0]}`); } } - // If everything is green but merge still blocked (e.g. review requirement), try admin merge. - if (pendingChecks === 0 && mergeStateStatus === 'BLOCKED') { + // If checks have materialized and everything is green but merge is still blocked + // (e.g. review requirement), try admin merge. Do not do this before checks exist. + if (sawAnyChecks && checks.length > 0 && pendingChecks === 0 && mergeStateStatus === 'BLOCKED') { try { run(`gh pr merge ${prNumber} --repo ${slug} --merge --admin --delete-branch`.trim(), repoDir, dryRun); } catch (err) { - const message = String(err.message || err); - if (!/required status checks are expected/i.test(message) && !/Repository rule violations found/i.test(message)) { + if (!isTransientRuleViolation(err)) { throw err; } + console.log(' Admin merge blocked while checks/rules are still settling; will retry.'); } } From d2890bcba8bfe99cccab2ec45b0583d67cfd8d5d Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 01:35:47 +0100 Subject: [PATCH 10/29] chore(release): checkpoint blocked-checks issue and fail fast on stalled PR state --- RELEASE-STATUS.md | 32 ++++++++++++++++++++++++++++++-- scripts/release-orchestrator.js | 15 +++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/RELEASE-STATUS.md b/RELEASE-STATUS.md index a0de1fd..776205e 100644 --- a/RELEASE-STATUS.md +++ b/RELEASE-STATUS.md @@ -1,7 +1,35 @@ # Release Orchestrator - Current Status -**Date:** March 19, 2026 -**Status:** ⚠ Stable flow semantics clarified (publish-from-main objective documented) +**Date:** March 20, 2026 +**Status:** ⚠ PR merge blocked by required-checks initialization gap on some repos + +--- + +## Checkpoint (March 20, 2026) + +Observed repeatedly on `SolidOS/solid-logic` release PRs (`#223`, `#224`, `#225`): + +``` +state=OPEN, mergeState=BLOCKED, pendingChecks=0, failingChecks=0 +GraphQL: Repository rule violations found +2 of 2 required status checks are expected. +``` + +Interpretation: +- The repo rules require checks, but those check runs are not yet materialized for the PR at the moment merge is attempted. +- This is distinct from failing checks; it is an "expected checks not started/visible yet" condition. + +Mitigation now in orchestrator: +- Retry logic for auto-merge and admin-merge paths. +- Transient rule-violation parsing now includes `message`, `stderr`, and `stdout`. +- New fail-fast diagnostic: if PR remains `BLOCKED` with no check entries for ~2 minutes, abort with explicit guidance instead of waiting silently. + +Tomorrow's investigation checklist: +1. Inspect branch protection/ruleset in `SolidOS/solid-logic` and list exact required check names. +2. Verify those workflows trigger on `pull_request` for `main` and for release branch naming pattern. +3. Confirm no `paths`/`paths-ignore` filters are excluding the release PR diff. +4. Verify required checks match current workflow job names exactly (renamed jobs can cause "expected" forever). +5. Re-run `stable-publish` after rules/workflow alignment and confirm PR auto-merges. --- diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 2660b0c..c824d97 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -470,6 +470,8 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = const startTime = Date.now(); let finalMergedAt = ''; let sawAnyChecks = false; + let blockedNoChecksCycles = 0; + const blockedNoChecksLimit = 8; // ~2 minutes at 15s polling const isTransientRuleViolation = (err) => { const text = [ @@ -518,6 +520,12 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = ` PR #${prNumber}: state=${state}, mergeState=${mergeStateStatus}, review=${reviewDecision}, pendingChecks=${pendingChecks}, failingChecks=${failingChecks}` ); + if (mergeStateStatus === 'BLOCKED' && checks.length === 0 && pendingChecks === 0) { + blockedNoChecksCycles += 1; + } else { + blockedNoChecksCycles = 0; + } + if (mergedAt) { finalMergedAt = mergedAt; break; @@ -531,6 +539,13 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = throw new Error(`PR #${prNumber} has failing required checks.`); } + if (blockedNoChecksCycles >= blockedNoChecksLimit) { + throw new Error( + `PR #${prNumber} stayed BLOCKED with no status checks for ${(blockedNoChecksCycles * pollInterval) / 1000}s. ` + + `This usually means required checks are configured but not running for this PR (workflow trigger/path filter/permissions mismatch).` + ); + } + // Retry requesting auto-merge once checks have started appearing. if (!autoMergeRequested && (pendingChecks > 0 || sawAnyChecks)) { try { From bd0b265ac5e3edbc3d4d6d5569537e639ad2f66f Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 18:23:53 +0100 Subject: [PATCH 11/29] chore(release): print required status check contexts for blocked PRs --- scripts/release-orchestrator.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index c824d97..c3a81ce 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -446,6 +446,24 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = return { status: 'skip', reason: 'no-pr' }; } + let requiredCheckContexts = []; + try { + const rawContexts = runQuiet( + `gh api repos/${slug}/branches/${baseBranch}/protection --jq '.required_status_checks.checks[].context'`, + repoDir + ); + requiredCheckContexts = String(rawContexts || '') + .split('\n') + .map((s) => s.trim()) + .filter(Boolean); + if (requiredCheckContexts.length > 0) { + console.log(`Required status checks on ${baseBranch}: ${requiredCheckContexts.join(', ')}`); + } + } catch (err) { + // Rulesets API visibility varies by token/repo configuration; keep merge flow working even if introspection fails. + console.log(`Warning: Could not read required status checks for ${baseBranch}: ${err.message}`); + } + console.log(`Found PR #${prNumber}. Requesting auto-merge...`); let autoMergeRequested = false; try { @@ -540,9 +558,13 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = } if (blockedNoChecksCycles >= blockedNoChecksLimit) { + const requiredLabel = requiredCheckContexts.length > 0 + ? ` Required checks configured: ${requiredCheckContexts.join(', ')}.` + : ''; throw new Error( `PR #${prNumber} stayed BLOCKED with no status checks for ${(blockedNoChecksCycles * pollInterval) / 1000}s. ` + - `This usually means required checks are configured but not running for this PR (workflow trigger/path filter/permissions mismatch).` + `This usually means required checks are configured but not running for this PR (workflow trigger/path filter/permissions mismatch).` + + requiredLabel ); } From 09ab7babac72c468a65418ed6753eeaff5248152 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 18:37:06 +0100 Subject: [PATCH 12/29] fix(release): trigger target CI when required checks are missing --- scripts/release-orchestrator.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index c3a81ce..d1aafaf 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -490,6 +490,8 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = let sawAnyChecks = false; let blockedNoChecksCycles = 0; const blockedNoChecksLimit = 8; // ~2 minutes at 15s polling + let ciKickoffAttempted = false; + const requiredChecksWorkflow = options.requiredChecksWorkflow || 'ci.yml'; const isTransientRuleViolation = (err) => { const text = [ @@ -544,6 +546,19 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = blockedNoChecksCycles = 0; } + // If the PR is blocked and no checks have appeared, proactively trigger CI in the target repo. + // This helps repos where required checks are configured but PR-triggered runs are delayed or skipped. + if (!ciKickoffAttempted && blockedNoChecksCycles >= 2) { + try { + run(`gh workflow run ${requiredChecksWorkflow} --repo ${slug} --ref ${headBranch}`.trim(), repoDir, dryRun); + ciKickoffAttempted = true; + console.log(` Triggered ${requiredChecksWorkflow} on ${headBranch} to materialize required checks.`); + } catch (err) { + ciKickoffAttempted = true; + console.log(` Warning: Could not trigger ${requiredChecksWorkflow} on ${headBranch}: ${err.message}`); + } + } + if (mergedAt) { finalMergedAt = mergedAt; break; @@ -1434,7 +1449,10 @@ function main() { maybeCreatePullRequest(repoDir, repo, branch, stablePublishReleaseBranch, dryRun, { required: true }); console.log('Step 2/3: Auto-merging release PR...'); - waitForPRMerge(repoDir, repo, stablePublishReleaseBranch, branch, dryRun, { required: true }); + waitForPRMerge(repoDir, repo, stablePublishReleaseBranch, branch, dryRun, { + required: true, + requiredChecksWorkflow: effectiveModeConfig.requiredChecksWorkflow || 'ci.yml' + }); ensureBranch(repoDir, branch, dryRun); From 93b14ca8fc6b2bb40eaad9f1820138057d622de2 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 18:45:46 +0100 Subject: [PATCH 13/29] fix(release): trigger PR CI by force-pushing empty commit --- scripts/release-orchestrator.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index d1aafaf..e4489d5 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -546,16 +546,17 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = blockedNoChecksCycles = 0; } - // If the PR is blocked and no checks have appeared, proactively trigger CI in the target repo. - // This helps repos where required checks are configured but PR-triggered runs are delayed or skipped. + // If the PR is blocked and no checks have appeared, force-push an empty commit to trigger PR CI. + // This re-triggers the pull_request event and materializes required checks on the PR. if (!ciKickoffAttempted && blockedNoChecksCycles >= 2) { try { - run(`gh workflow run ${requiredChecksWorkflow} --repo ${slug} --ref ${headBranch}`.trim(), repoDir, dryRun); + run(`git commit --allow-empty -m "Trigger CI checks"`, repoDir, dryRun); + run(`git push -f origin ${headBranch}`, repoDir, dryRun); ciKickoffAttempted = true; - console.log(` Triggered ${requiredChecksWorkflow} on ${headBranch} to materialize required checks.`); + console.log(` Pushed empty commit to ${headBranch} to trigger PR CI checks.`); } catch (err) { ciKickoffAttempted = true; - console.log(` Warning: Could not trigger ${requiredChecksWorkflow} on ${headBranch}: ${err.message}`); + console.log(` Warning: Could not push to ${headBranch}: ${err.message}`); } } From 375b0cc86f595120196135f96b3c5ec2a4f1cbd7 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 18:53:38 +0100 Subject: [PATCH 14/29] fix(release): clean working tree after force-push before checkout --- scripts/release-orchestrator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index e4489d5..20f2905 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -621,6 +621,10 @@ function waitForPRMerge(repoDir, repo, headBranch, baseBranch, dryRun, options = console.log(`PR #${prNumber} merged at ${finalMergedAt}.`); + // Clean working tree before switching branches (force-push of empty commit may leave local changes) + run(`git reset --hard`, repoDir, dryRun); + run(`git clean -fd`, repoDir, dryRun); + // Fetch the updated main branch run(`git fetch origin ${baseBranch}`, repoDir, dryRun); run(`git checkout ${baseBranch}`, repoDir, dryRun); From 2fae4ef115f5692f36210b09c6785e1b0b84260f Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 18:55:51 +0100 Subject: [PATCH 15/29] docs: checkpoint successful stable-publish PR merge flow --- RELEASE-STATUS.md | 49 +++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/RELEASE-STATUS.md b/RELEASE-STATUS.md index 776205e..8ace68e 100644 --- a/RELEASE-STATUS.md +++ b/RELEASE-STATUS.md @@ -1,38 +1,45 @@ # Release Orchestrator - Current Status **Date:** March 20, 2026 -**Status:** ⚠ PR merge blocked by required-checks initialization gap on some repos +**Status:** ✅ `stable-publish` PR merge flow working (CI trigger via empty commit) --- -## Checkpoint (March 20, 2026) +## Checkpoint (March 20, 2026 - final) -Observed repeatedly on `SolidOS/solid-logic` release PRs (`#223`, `#224`, `#225`): +**BREAKTHROUGH**: `stable-publish` now successfully creates release PR, triggers required CI checks, merges, and completes step 2/3. +### Problem Solved +- Required checks (`build (22)` / `build (24)`) were configured on `main` ruleset but not triggering for release PRs. +- Cause: `ci.yml` listeners to `pull_request` event, which only fires on natural PR creation; orchestrator was trying workflow dispatch (wrong event type). +- Solution: Force-push an empty commit to the release branch → GitHub re-fires `pull_request` event → CI runs and checks post back to PR → merge proceeds. + +### Changes in orchestrator +- Commit `375b0cc`: Added `git reset --hard && git clean -fd` before checkout to handle uncommitted changes left after empty-commit force-push. +- Earlier commits: CI-trigger via empty commit (commit `93b14ca`), transient error handling, required-checks diagnostics. + +### Result (from last run, PR #228) ``` -state=OPEN, mergeState=BLOCKED, pendingChecks=0, failingChecks=0 -GraphQL: Repository rule violations found -2 of 2 required status checks are expected. +Found PR #228. Requesting auto-merge... + PR #228: state=OPEN, mergeState=BLOCKED, review=UNKNOWN, pendingChecks=0, failingChecks=0 + PR #228: state=OPEN, mergeState=BLOCKED, review=UNKNOWN, pendingChecks=0, failingChecks=0 +... (2 poll cycles) ... + Pushed empty commit to release/solid-logic-202603201747 to trigger PR CI checks. + PR #228: state=OPEN, mergeState=BLOCKED, review=UNKNOWN, pendingChecks=2, failingChecks=0 + PR #228: state=OPEN, mergeState=BLOCKED, review=UNKNOWN, pendingChecks=1, failingChecks=0 + PR #228: state=MERGED, mergeState=UNKNOWN, review=UNKNOWN, pendingChecks=1, failingChecks=0 +PR #228 merged at 2026-03-20T17:49:44Z. ``` +✅ **PR merged successfully.** -Interpretation: -- The repo rules require checks, but those check runs are not yet materialized for the PR at the moment merge is attempted. -- This is distinct from failing checks; it is an "expected checks not started/visible yet" condition. +The only remaining error was `git checkout main` due to dirty working tree, which is now fixed by commit `375b0cc`. -Mitigation now in orchestrator: -- Retry logic for auto-merge and admin-merge paths. -- Transient rule-violation parsing now includes `message`, `stderr`, and `stdout`. -- New fail-fast diagnostic: if PR remains `BLOCKED` with no check entries for ~2 minutes, abort with explicit guidance instead of waiting silently. - -Tomorrow's investigation checklist: -1. Inspect branch protection/ruleset in `SolidOS/solid-logic` and list exact required check names. -2. Verify those workflows trigger on `pull_request` for `main` and for release branch naming pattern. -3. Confirm no `paths`/`paths-ignore` filters are excluding the release PR diff. -4. Verify required checks match current workflow job names exactly (renamed jobs can cause "expected" forever). -5. Re-run `stable-publish` after rules/workflow alignment and confirm PR auto-merges. +### Future optimization +- User suggestion: Run CI earlier (before version bump) so it's ready when PR opens, avoiding need for empty-commit re-trigger. +- Feasibility: Needs testing; could reduce merge wait time from ~2min to ~30s. +- Implementation: Create release branch, push immediately, PR opens → CI runs → version bump → PR auto-merges (if CI passes quickly). --- - ## Latest Test Run Results ``` From 4577f6432b7ce5ba2e6c6286e25dbbcb64e4c993 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 18:56:55 +0100 Subject: [PATCH 16/29] fix(release): lock dependencies with caret prefix by default to allow compatible updates --- scripts/release-orchestrator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 20f2905..a4825b5 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -786,7 +786,7 @@ function lockStableDependencyVersions(repoDir, repo, config, modeConfig, dryRun) } const lockFields = modeConfig.lockDependencyFields || config.lockDependencyFields || ['dependencies', 'devDependencies']; - const versionPrefix = modeConfig.lockDependencyPrefix ?? config.lockDependencyPrefix ?? ''; + const versionPrefix = modeConfig.lockDependencyPrefix ?? config.lockDependencyPrefix ?? '^'; const packageJson = readJson(pkgPath); let changed = false; From 3ba97ab300a910f31376ea961fa938d2f8d2b09f Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 19:37:46 +0100 Subject: [PATCH 17/29] fix(release): commit version bump when gitTag:false so release branch is not missing the bump --- scripts/release-orchestrator.js | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index a4825b5..c7c8fcd 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -955,15 +955,36 @@ function bumpStableVersion(repoDir, modeConfig, dryRun) { }); const bump = modeConfig.versionBump || 'patch'; - if (modeConfig.gitTag === false) { - run(`npm version ${bump} --no-git-tag-version --ignore-scripts`, repoDir, dryRun); - } else { - run(`npm version ${bump} -m "Release %s" --ignore-scripts`, repoDir, dryRun); + const pkg = getPackageJson(repoDir); + const packageName = pkg ? pkg.name : null; + + const doVersionBump = () => { + if (modeConfig.gitTag === false) { + run(`npm version ${bump} --no-git-tag-version --ignore-scripts`, repoDir, dryRun); + // --no-git-tag-version only edits package.json; commit the change explicitly. + run(`git add package.json package-lock.json`, repoDir, dryRun); + const bumpedVer = getPackageVersion(repoDir); + run(`git commit -m "Release ${bumpedVer} [skip ci]" --allow-empty`, repoDir, dryRun); + } else { + run(`npm version ${bump} -m "Release %s" --ignore-scripts`, repoDir, dryRun); + } + }; + + doVersionBump(); + + // If a previous partial run already published this version to npm, bump once more to get a clean version. + if (!dryRun && packageName) { + const bumpedVersion = getPackageVersion(repoDir); + if (packageVersionExists(packageName, bumpedVersion, repoDir)) { + console.log(`Version ${packageName}@${bumpedVersion} already exists on npm (previous partial run). Bumping again...`); + doVersionBump(); + console.log(`New version: ${getPackageVersion(repoDir)}`); + } } - const pkg = getPackageJson(repoDir); + const updatedPkg = getPackageJson(repoDir); return { - packageName: pkg ? pkg.name : null, + packageName: updatedPkg ? updatedPkg.name : null, version: getPackageVersion(repoDir), tag: modeConfig.npmTag || 'latest' }; From 8b1a5eff079b4aed008e22bf9ad1c1590d689e91 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 20 Mar 2026 20:13:54 +0100 Subject: [PATCH 18/29] refactor(release): delegate stable-publish latest publish to repo CI --- scripts/release-orchestrator.js | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index c7c8fcd..95c2403 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -955,9 +955,6 @@ function bumpStableVersion(repoDir, modeConfig, dryRun) { }); const bump = modeConfig.versionBump || 'patch'; - const pkg = getPackageJson(repoDir); - const packageName = pkg ? pkg.name : null; - const doVersionBump = () => { if (modeConfig.gitTag === false) { run(`npm version ${bump} --no-git-tag-version --ignore-scripts`, repoDir, dryRun); @@ -972,16 +969,6 @@ function bumpStableVersion(repoDir, modeConfig, dryRun) { doVersionBump(); - // If a previous partial run already published this version to npm, bump once more to get a clean version. - if (!dryRun && packageName) { - const bumpedVersion = getPackageVersion(repoDir); - if (packageVersionExists(packageName, bumpedVersion, repoDir)) { - console.log(`Version ${packageName}@${bumpedVersion} already exists on npm (previous partial run). Bumping again...`); - doVersionBump(); - console.log(`New version: ${getPackageVersion(repoDir)}`); - } - } - const updatedPkg = getPackageJson(repoDir); return { packageName: updatedPkg ? updatedPkg.name : null, @@ -1482,11 +1469,15 @@ function main() { ensureBranch(repoDir, branch, dryRun); - console.log('Step 3/3: Publishing packages from merged branch...'); - const result = publishPreparedStable(repoDir, effectiveModeConfig, dryRun, effectiveBuildCmd); + console.log('Step 3/3: Publish delegated to repository CI on main.'); + const result = { + packageName: pkg ? pkg.name : null, + version: getPackageVersion(repoDir), + tag: effectiveModeConfig.npmTag || 'latest' + }; summary.push({ name: repo.name, - status: dryRun ? 'dry-run' : 'published', + status: dryRun ? 'dry-run' : 'published-by-repo-ci', packageName: result.packageName || null, version: result.version, tag: result.tag, From 5c1ad7ec7c0a2e5762f25a48c2ac7467e37dfcc5 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Sat, 21 Mar 2026 15:22:48 +0100 Subject: [PATCH 19/29] pane-registry release --- RELEASE-STATUS.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/RELEASE-STATUS.md b/RELEASE-STATUS.md index 8ace68e..ec6e7b7 100644 --- a/RELEASE-STATUS.md +++ b/RELEASE-STATUS.md @@ -1,7 +1,28 @@ # Release Orchestrator - Current Status **Date:** March 20, 2026 -**Status:** ✅ `stable-publish` PR merge flow working (CI trigger via empty commit) +**Status:** ✅ Single source of truth adopted for `latest` publish: repository `ci.yml` on `main` + +--- + +## Checkpoint (March 20, 2026 - latest) + +Final decision for stable releases: +- `stable-publish` in orchestrator now stops after PR merge. +- `latest` publication is delegated to each target repository's `ci.yml` on `main`. +- This removes duplicate publish attempts (orchestrator + repo CI) and keeps one publisher for `latest`. + +Implemented commit in `solidos`: +- `8b1a5ef` — `refactor(release): delegate stable-publish latest publish to repo CI` + +Versioning policy clarification: +- Source of truth for release version is `main`, not npm latest lookup. +- `stable-publish` should bump from `main` state and merge that bump via PR. + +Next run planned: +1. Execute `release.yml` on branch `release` for `pane-registry` in `stable-publish` mode. +2. Expect orchestrator summary status `published-by-repo-ci` (no direct npm publish in step 3). +3. Confirm `pane-registry` `ci.yml` on `main` performs the single `latest` publish. --- From bc2ef60f1c6747acf038216b0110e6b9bd754847 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Sat, 21 Mar 2026 15:40:59 +0100 Subject: [PATCH 20/29] release pane-registry xonfig --- release.pane-registry.config.json | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 release.pane-registry.config.json diff --git a/release.pane-registry.config.json b/release.pane-registry.config.json new file mode 100644 index 0000000..855d822 --- /dev/null +++ b/release.pane-registry.config.json @@ -0,0 +1,34 @@ +{ + "defaultBranch": "main", + "skipIfNoDiff": true, + "defaultInstall": "npm install", + "defaultTest": "npm test", + "defaultBuild": "npm run build", + "modes": { + "stable": { + "branch": "main", + "versionBump": "patch", + "npmTag": "latest", + "gitTag": true, + "gitPush": true + }, + "test": { + "branch": "dev", + "versionBump": "prerelease", + "preid": "test", + "npmTag": "test", + "gitTag": false, + "gitPush": false + } + }, + "repos": [ + { + "name": "pane-registry", + "path": "workspaces/pane-registry", + "repo": "https://github.com/SolidOS/pane-registry.git", + "afterInstall": [ + "npm install solid-logic@latest rdflib@latest" + ] + } + ] +} From 476b133895425c24867afb44d4176cdb118b7203 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Sat, 21 Mar 2026 17:03:41 +0100 Subject: [PATCH 21/29] feat(release): wait for repo CI success before npm publish visibility --- scripts/release-orchestrator.js | 87 ++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 95c2403..7396950 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -659,6 +659,70 @@ function ensureCiPushAccess(repoDir, branch, dryRun, modeConfig = {}) { } } +function waitForWorkflowSuccess(repoDir, repo, workflowRef, branch, headSha, dryRun, options = {}) { + if (dryRun) { + console.log(`[dry-run] Would wait for workflow ${workflowRef} on ${branch} at ${headSha}.`); + return { status: 'dry-run' }; + } + + const slug = parseGitHubRepoSlug(repo.repo || runQuiet('git remote get-url origin', repoDir)); + if (!slug) { + throw new Error(`Could not determine GitHub repo slug. Cannot wait for workflow ${workflowRef}.`); + } + + try { + runQuiet('gh --version', repoDir); + } catch (err) { + throw new Error(`gh CLI not available in runner. Cannot wait for workflow ${workflowRef}.`); + } + + const maxWaitTime = options.timeoutMs || 20 * 60 * 1000; + const pollInterval = options.pollIntervalMs || 15 * 1000; + const startTime = Date.now(); + + while ((Date.now() - startTime) < maxWaitTime) { + let runs = []; + try { + const query = `gh run list --repo ${slug} --workflow ${workflowRef} --branch ${branch} --event push --limit 20 --json databaseId,headSha,status,conclusion,url,displayTitle`; + runs = JSON.parse(runQuiet(query, repoDir) || '[]'); + } catch (err) { + console.log(` Warning reading workflow state for ${workflowRef}: ${err.message}`); + sleepMs(pollInterval); + continue; + } + + const matchingRun = Array.isArray(runs) + ? runs.find((runInfo) => String(runInfo.headSha || '').trim() === String(headSha || '').trim()) + : null; + + if (!matchingRun) { + console.log(` Waiting for workflow ${workflowRef} on ${branch} for commit ${headSha} to start...`); + sleepMs(pollInterval); + continue; + } + + const status = String(matchingRun.status || 'unknown'); + const conclusion = String(matchingRun.conclusion || ''); + console.log(` Workflow ${workflowRef}: status=${status}, conclusion=${conclusion || 'pending'}`); + + if (status !== 'completed') { + sleepMs(pollInterval); + continue; + } + + if (conclusion === 'success') { + return { status: 'success', runId: matchingRun.databaseId || null, url: matchingRun.url || null }; + } + + throw new Error( + `Workflow ${workflowRef} failed for ${slug}@${headSha} with conclusion=${conclusion || 'unknown'}.` + + `${matchingRun.url ? ` See ${matchingRun.url}` : ''}` + ); + } + + throw new Error(`Timed out waiting for workflow ${workflowRef} on ${branch} for commit ${headSha}.`); +} + function getAheadBehind(repoDir, branch) { const raw = runQuiet(`git rev-list --left-right --count origin/${branch}...HEAD`, repoDir); const [behind, ahead] = raw.split('\t').map(Number); @@ -1469,12 +1533,33 @@ function main() { ensureBranch(repoDir, branch, dryRun); - console.log('Step 3/3: Publish delegated to repository CI on main.'); + console.log('Step 3/3: Waiting for repository CI publish on main...'); const result = { packageName: pkg ? pkg.name : null, version: getPackageVersion(repoDir), tag: effectiveModeConfig.npmTag || 'latest' }; + + if (!dryRun && result.packageName && result.version) { + const workflowRef = effectiveModeConfig.repoCiWorkflow || effectiveModeConfig.requiredChecksWorkflow || 'ci.yml'; + const headSha = runQuiet('git rev-parse HEAD', repoDir).trim(); + console.log(`Waiting for workflow ${workflowRef} on ${branch} to succeed for commit ${headSha}...`); + waitForWorkflowSuccess(repoDir, repo, workflowRef, branch, headSha, dryRun, { + timeoutMs: effectiveModeConfig.repoCiWorkflowTimeoutMs || 20 * 60 * 1000, + pollIntervalMs: effectiveModeConfig.repoCiWorkflowPollIntervalMs || 15000 + }); + + const timeoutMs = effectiveModeConfig.repoCiPublishTimeoutMs || 10 * 60 * 1000; + const pollIntervalMs = effectiveModeConfig.repoCiPublishPollIntervalMs || 5000; + console.log(`Waiting for ${result.packageName}@${result.version} to be available on npm...`); + const registryReady = waitForNpmVersion(result.packageName, result.version, repoDir, timeoutMs, pollIntervalMs); + if (!registryReady) { + throw new Error( + `Timed out waiting for repository CI to publish ${result.packageName}@${result.version} to npm.` + ); + } + } + summary.push({ name: repo.name, status: dryRun ? 'dry-run' : 'published-by-repo-ci', From 79427242bdae70e3dc9a4b3d75fa5c731c5dd67f Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Sat, 21 Mar 2026 19:05:40 +0100 Subject: [PATCH 22/29] fix(release): always emit release summary even when a repo fails --- scripts/release-orchestrator.js | 54 +++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/scripts/release-orchestrator.js b/scripts/release-orchestrator.js index 7396950..7c7a510 100644 --- a/scripts/release-orchestrator.js +++ b/scripts/release-orchestrator.js @@ -1293,6 +1293,26 @@ function publishTest(repoDir, modeConfig, dryRun, buildCmd) { return { packageName: name, version, tag }; } +function emitReleaseSummary(mode, dryRun, summary, summaryPath) { + console.log('\nRelease summary:'); + for (const item of summary) { + if (item.status === 'published' || item.status === 'published-by-repo-ci' || item.status === 'dry-run') { + console.log(`- ${item.name}: ${item.status} ${item.version || ''} (${item.tag || 'latest'})`.trim()); + } else { + console.log(`- ${item.name}: ${item.status} (${item.reason})`); + } + } + + const summaryPayload = { + mode, + dryRun, + generatedAt: new Date().toISOString(), + items: summary + }; + fs.writeFileSync(summaryPath, JSON.stringify(summaryPayload, null, 2)); + console.log(`Summary written to ${summaryPath}`); +} + function main() { preferGhToken(); const args = parseArgs(process.argv.slice(2)); @@ -1331,8 +1351,11 @@ function main() { } const summary = []; + let fatalError = null; + try { for (const repo of config.repos) { + try { const repoDir = path.resolve(configDir, repo.path); const branch = branchOverride || repo.branch || modeConfig.branch || config.defaultBranch || 'main'; const effectiveModeConfig = { ...modeConfig, branch }; @@ -1583,25 +1606,24 @@ function main() { } runSteps(repo.afterPublish, repoDir, dryRun); - } - - console.log('\nRelease summary:'); - for (const item of summary) { - if (item.status === 'published' || item.status === 'dry-run') { - console.log(`- ${item.name}: ${item.status} ${item.version || ''} (${item.tag || 'latest'})`.trim()); - } else { - console.log(`- ${item.name}: ${item.status} (${item.reason})`); + } catch (err) { + summary.push({ + name: repo.name, + status: 'failed', + reason: err.message || String(err) + }); + throw err; } } + } catch (err) { + fatalError = err; + } finally { + emitReleaseSummary(mode, dryRun, summary, summaryPath); + } - const summaryPayload = { - mode, - dryRun, - generatedAt: new Date().toISOString(), - items: summary - }; - fs.writeFileSync(summaryPath, JSON.stringify(summaryPayload, null, 2)); - console.log(`Summary written to ${summaryPath}`); + if (fatalError) { + throw fatalError; + } } try { From a78607d2b45d00ec793e8e82acf82f1ccb9e0cc3 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Mon, 23 Mar 2026 11:48:42 +0100 Subject: [PATCH 23/29] release from solid-ui --- release.config.from-solid-ui.json | 115 ++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 release.config.from-solid-ui.json diff --git a/release.config.from-solid-ui.json b/release.config.from-solid-ui.json new file mode 100644 index 0000000..7c20a0b --- /dev/null +++ b/release.config.from-solid-ui.json @@ -0,0 +1,115 @@ +{ + "defaultBranch": "main", + "skipIfNoDiff": true, + "defaultInstall": "npm install", + "defaultTest": "npm test", + "defaultBuild": "npm run build", + "modes": { + "stable": { + "branch": "main", + "versionBump": "patch", + "npmTag": "latest", + "gitTag": true, + "gitPush": true + }, + "test": { + "branch": "dev", + "versionBump": "prerelease", + "preid": "test", + "npmTag": "test", + "gitTag": false, + "gitPush": false + } + }, + "repos": [ + { + "name": "solid-ui", + "path": "workspaces/solid-ui", + "repo": "https://github.com/SolidOS/solid-ui.git", + "afterInstall": [ + "npm install pane-registry@latest solid-logic@latest rdflib@latest solid-namespace@latest" + ] + }, + { + "name": "activitystreams-pane", + "path": "workspaces/activitystreams-pane", + "repo": "https://github.com/SolidOS/activitystreams-pane.git", + "afterInstall": [ + "npm install solid-ui pane-registry solid-logic rdflib@latest" + ] + }, + { + "name": "chat-pane", + "path": "workspaces/chat-pane", + "repo": "https://github.com/SolidOS/chat-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "folder-pane", + "path": "workspaces/folder-pane", + "repo": "https://github.com/SolidOS/folder-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "issue-pane", + "path": "workspaces/issue-pane", + "repo": "https://github.com/SolidOS/issue-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "meeting-pane", + "path": "workspaces/meeting-pane", + "repo": "https://github.com/SolidOS/meeting-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "contacts-pane", + "path": "workspaces/contacts-pane", + "repo": "https://github.com/SolidOS/contacts-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "profile-pane", + "path": "workspaces/profile-pane", + "repo": "https://github.com/SolidOS/profile-pane.git", + "afterInstall": [ + "npm install chat-pane solid-ui pane-registry solid-logic rdflib@latest" + ] + }, + { + "name": "source-pane", + "path": "workspaces/source-pane", + "repo": "https://github.com/SolidOS/source-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "solid-panes", + "path": "workspaces/solid-panes", + "repo": "https://github.com/SolidOS/solid-panes.git", + "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", + "afterInstall": [ + "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" + ] + }, + { + "name": "mashlib", + "path": "workspaces/mashlib", + "repo": "https://github.com/SolidOS/mashlib.git", + "afterInstall": [ + "npm install solid-panes solid-ui solid-logic rdflib@latest" + ] + } + ] +} From 4d330f3bb3e0f6b03c42e063b3b6a32531c6da7e Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Mon, 23 Mar 2026 12:11:16 +0100 Subject: [PATCH 24/29] release from chat-pane --- release.config.from-chat-pane.json | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 release.config.from-chat-pane.json diff --git a/release.config.from-chat-pane.json b/release.config.from-chat-pane.json new file mode 100644 index 0000000..67db46a --- /dev/null +++ b/release.config.from-chat-pane.json @@ -0,0 +1,99 @@ +{ + "defaultBranch": "main", + "skipIfNoDiff": true, + "defaultInstall": "npm install", + "defaultTest": "npm test", + "defaultBuild": "npm run build", + "modes": { + "stable": { + "branch": "main", + "versionBump": "patch", + "npmTag": "latest", + "gitTag": true, + "gitPush": true + }, + "test": { + "branch": "dev", + "versionBump": "prerelease", + "preid": "test", + "npmTag": "test", + "gitTag": false, + "gitPush": false + } + }, + "repos": [ + { + "name": "chat-pane", + "path": "workspaces/chat-pane", + "repo": "https://github.com/SolidOS/chat-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "folder-pane", + "path": "workspaces/folder-pane", + "repo": "https://github.com/SolidOS/folder-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "issue-pane", + "path": "workspaces/issue-pane", + "repo": "https://github.com/SolidOS/issue-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "meeting-pane", + "path": "workspaces/meeting-pane", + "repo": "https://github.com/SolidOS/meeting-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "contacts-pane", + "path": "workspaces/contacts-pane", + "repo": "https://github.com/SolidOS/contacts-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "profile-pane", + "path": "workspaces/profile-pane", + "repo": "https://github.com/SolidOS/profile-pane.git", + "afterInstall": [ + "npm install chat-pane solid-ui pane-registry solid-logic rdflib@latest" + ] + }, + { + "name": "source-pane", + "path": "workspaces/source-pane", + "repo": "https://github.com/SolidOS/source-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "solid-panes", + "path": "workspaces/solid-panes", + "repo": "https://github.com/SolidOS/solid-panes.git", + "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", + "afterInstall": [ + "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" + ] + }, + { + "name": "mashlib", + "path": "workspaces/mashlib", + "repo": "https://github.com/SolidOS/mashlib.git", + "afterInstall": [ + "npm install solid-panes solid-ui solid-logic rdflib@latest" + ] + } + ] +} From d9f53fe8e4ff67fe6f8dd662e3061ed731897584 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Mon, 23 Mar 2026 12:59:24 +0100 Subject: [PATCH 25/29] release from-folder-pane --- release.config.from-folder-pane.json | 91 ++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 release.config.from-folder-pane.json diff --git a/release.config.from-folder-pane.json b/release.config.from-folder-pane.json new file mode 100644 index 0000000..935ac7b --- /dev/null +++ b/release.config.from-folder-pane.json @@ -0,0 +1,91 @@ +{ + "defaultBranch": "main", + "skipIfNoDiff": true, + "defaultInstall": "npm install", + "defaultTest": "npm test", + "defaultBuild": "npm run build", + "modes": { + "stable": { + "branch": "main", + "versionBump": "patch", + "npmTag": "latest", + "gitTag": true, + "gitPush": true + }, + "test": { + "branch": "dev", + "versionBump": "prerelease", + "preid": "test", + "npmTag": "test", + "gitTag": false, + "gitPush": false + } + }, + "repos": [ + { + "name": "folder-pane", + "path": "workspaces/folder-pane", + "repo": "https://github.com/SolidOS/folder-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "issue-pane", + "path": "workspaces/issue-pane", + "repo": "https://github.com/SolidOS/issue-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "meeting-pane", + "path": "workspaces/meeting-pane", + "repo": "https://github.com/SolidOS/meeting-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "contacts-pane", + "path": "workspaces/contacts-pane", + "repo": "https://github.com/SolidOS/contacts-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "profile-pane", + "path": "workspaces/profile-pane", + "repo": "https://github.com/SolidOS/profile-pane.git", + "afterInstall": [ + "npm install chat-pane solid-ui pane-registry solid-logic rdflib@latest" + ] + }, + { + "name": "source-pane", + "path": "workspaces/source-pane", + "repo": "https://github.com/SolidOS/source-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "solid-panes", + "path": "workspaces/solid-panes", + "repo": "https://github.com/SolidOS/solid-panes.git", + "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", + "afterInstall": [ + "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" + ] + }, + { + "name": "mashlib", + "path": "workspaces/mashlib", + "repo": "https://github.com/SolidOS/mashlib.git", + "afterInstall": [ + "npm install solid-panes solid-ui solid-logic rdflib@latest" + ] + } + ] +} From 42f380f4208d8318af4ffe47d182be3a846507b8 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Mon, 23 Mar 2026 13:20:06 +0100 Subject: [PATCH 26/29] from issue-pane --- release.config.from-issue-pane.json | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 release.config.from-issue-pane.json diff --git a/release.config.from-issue-pane.json b/release.config.from-issue-pane.json new file mode 100644 index 0000000..bfbf76d --- /dev/null +++ b/release.config.from-issue-pane.json @@ -0,0 +1,83 @@ +{ + "defaultBranch": "main", + "skipIfNoDiff": true, + "defaultInstall": "npm install", + "defaultTest": "npm test", + "defaultBuild": "npm run build", + "modes": { + "stable": { + "branch": "main", + "versionBump": "patch", + "npmTag": "latest", + "gitTag": true, + "gitPush": true + }, + "test": { + "branch": "dev", + "versionBump": "prerelease", + "preid": "test", + "npmTag": "test", + "gitTag": false, + "gitPush": false + } + }, + "repos": [ + { + "name": "issue-pane", + "path": "workspaces/issue-pane", + "repo": "https://github.com/SolidOS/issue-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "meeting-pane", + "path": "workspaces/meeting-pane", + "repo": "https://github.com/SolidOS/meeting-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "contacts-pane", + "path": "workspaces/contacts-pane", + "repo": "https://github.com/SolidOS/contacts-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "profile-pane", + "path": "workspaces/profile-pane", + "repo": "https://github.com/SolidOS/profile-pane.git", + "afterInstall": [ + "npm install chat-pane solid-ui pane-registry solid-logic rdflib@latest" + ] + }, + { + "name": "source-pane", + "path": "workspaces/source-pane", + "repo": "https://github.com/SolidOS/source-pane.git", + "afterInstall": [ + "npm install solid-ui solid-logic rdflib@latest" + ] + }, + { + "name": "solid-panes", + "path": "workspaces/solid-panes", + "repo": "https://github.com/SolidOS/solid-panes.git", + "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", + "afterInstall": [ + "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" + ] + }, + { + "name": "mashlib", + "path": "workspaces/mashlib", + "repo": "https://github.com/SolidOS/mashlib.git", + "afterInstall": [ + "npm install solid-panes solid-ui solid-logic rdflib@latest" + ] + } + ] +} From 3a7cf9cc278d05192f45091e4394427f8395c1d4 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Mon, 23 Mar 2026 15:13:43 +0100 Subject: [PATCH 27/29] release from solid-panes --- release.config.from-solid-panes.json | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 release.config.from-solid-panes.json diff --git a/release.config.from-solid-panes.json b/release.config.from-solid-panes.json new file mode 100644 index 0000000..7aff768 --- /dev/null +++ b/release.config.from-solid-panes.json @@ -0,0 +1,43 @@ +{ + "defaultBranch": "main", + "skipIfNoDiff": true, + "defaultInstall": "npm install", + "defaultTest": "npm test", + "defaultBuild": "npm run build", + "modes": { + "stable": { + "branch": "main", + "versionBump": "patch", + "npmTag": "latest", + "gitTag": true, + "gitPush": true + }, + "test": { + "branch": "dev", + "versionBump": "prerelease", + "preid": "test", + "npmTag": "test", + "gitTag": false, + "gitPush": false + } + }, + "repos": [ + { + "name": "solid-panes", + "path": "workspaces/solid-panes", + "repo": "https://github.com/SolidOS/solid-panes.git", + "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", + "afterInstall": [ + "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" + ] + }, + { + "name": "mashlib", + "path": "workspaces/mashlib", + "repo": "https://github.com/SolidOS/mashlib.git", + "afterInstall": [ + "npm install solid-panes solid-ui solid-logic rdflib@latest" + ] + } + ] +} From b2d07829c72ef552c4cfadc3a88e4105fc9e1c6e Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Mon, 23 Mar 2026 15:21:20 +0100 Subject: [PATCH 28/29] remove broke build step --- release.config.from-solid-panes.json | 1 - 1 file changed, 1 deletion(-) diff --git a/release.config.from-solid-panes.json b/release.config.from-solid-panes.json index 7aff768..9ee734d 100644 --- a/release.config.from-solid-panes.json +++ b/release.config.from-solid-panes.json @@ -26,7 +26,6 @@ "name": "solid-panes", "path": "workspaces/solid-panes", "repo": "https://github.com/SolidOS/solid-panes.git", - "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", "afterInstall": [ "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" ] From 92472e36581e1efead09f02ac117c8bae6d5559c Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Mon, 23 Mar 2026 15:25:20 +0100 Subject: [PATCH 29/29] remove failing build step without npm ci --- release.config.from-chat-pane.json | 1 - release.config.from-folder-pane.json | 1 - release.config.from-issue-pane.json | 1 - release.config.from-solid-ui.json | 1 - release.config.json | 1 - 5 files changed, 5 deletions(-) diff --git a/release.config.from-chat-pane.json b/release.config.from-chat-pane.json index 67db46a..96af9da 100644 --- a/release.config.from-chat-pane.json +++ b/release.config.from-chat-pane.json @@ -82,7 +82,6 @@ "name": "solid-panes", "path": "workspaces/solid-panes", "repo": "https://github.com/SolidOS/solid-panes.git", - "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", "afterInstall": [ "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" ] diff --git a/release.config.from-folder-pane.json b/release.config.from-folder-pane.json index 935ac7b..15408cc 100644 --- a/release.config.from-folder-pane.json +++ b/release.config.from-folder-pane.json @@ -74,7 +74,6 @@ "name": "solid-panes", "path": "workspaces/solid-panes", "repo": "https://github.com/SolidOS/solid-panes.git", - "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", "afterInstall": [ "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" ] diff --git a/release.config.from-issue-pane.json b/release.config.from-issue-pane.json index bfbf76d..5fd3e0f 100644 --- a/release.config.from-issue-pane.json +++ b/release.config.from-issue-pane.json @@ -66,7 +66,6 @@ "name": "solid-panes", "path": "workspaces/solid-panes", "repo": "https://github.com/SolidOS/solid-panes.git", - "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", "afterInstall": [ "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" ] diff --git a/release.config.from-solid-ui.json b/release.config.from-solid-ui.json index 7c20a0b..83a9484 100644 --- a/release.config.from-solid-ui.json +++ b/release.config.from-solid-ui.json @@ -98,7 +98,6 @@ "name": "solid-panes", "path": "workspaces/solid-panes", "repo": "https://github.com/SolidOS/solid-panes.git", - "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", "afterInstall": [ "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" ] diff --git a/release.config.json b/release.config.json index e9c28ba..fe4ea8b 100644 --- a/release.config.json +++ b/release.config.json @@ -114,7 +114,6 @@ "name": "solid-panes", "path": "workspaces/solid-panes", "repo": "https://github.com/SolidOS/solid-panes.git", - "build": "npm run clean && npm run build-version && npm run typecheck && babel src --out-dir dist --extensions '.ts,.js' --source-maps && tsc --emitDeclarationOnly && npm run postbuild-js", "afterInstall": [ "npm install activitystreams-pane chat-pane contacts-pane folder-pane issue-pane meeting-pane pane-registry profile-pane source-pane solid-ui solid-logic solid-namespace@latest rdflib@latest" ]