From 33807b13f7befe64aec3a7f1b28511a3d2b03995 Mon Sep 17 00:00:00 2001 From: dzeusking-dev Date: Mon, 31 Aug 2026 07:43:11 +0200 Subject: [PATCH 1/9] ci: add guarded release tag workflow --- .github/workflows/create-release-tag.yml | 54 ++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/create-release-tag.yml diff --git a/.github/workflows/create-release-tag.yml b/.github/workflows/create-release-tag.yml new file mode 100644 index 0000000..428f643 --- /dev/null +++ b/.github/workflows/create-release-tag.yml @@ -0,0 +1,54 @@ +name: Create release tag + +on: + workflow_dispatch: + inputs: + tag: + description: Immutable semantic release tag, for example v0.2.2 + required: true + type: string + +permissions: + contents: write + +concurrency: + group: create-release-tag + cancel-in-progress: false + +jobs: + create: + name: Verify main and create immutable tag + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + name: github-release + steps: + - name: Check out current main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + fetch-depth: 0 + + - name: Verify and push tag + env: + RELEASE_TAG: ${{ inputs.tag }} + shell: bash + run: | + set -euo pipefail + [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] + package_version="$(node --input-type=commonjs -p "require('./package.json').version")" + [[ "${RELEASE_TAG}" == "v${package_version}" ]] + + release_sha="$(git rev-parse HEAD)" + git fetch --no-tags --force origin main:refs/remotes/origin/main + [[ "${release_sha}" == "$(git rev-parse refs/remotes/origin/main)" ]] + + if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then + echo "Tag ${RELEASE_TAG} already exists." >&2 + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "${RELEASE_TAG}" "${release_sha}" -m "PC FileBridge ${RELEASE_TAG}" + git push origin "refs/tags/${RELEASE_TAG}" From f6b1edf5a58d664ace2b962cd910e56ce0c5a40e Mon Sep 17 00:00:00 2001 From: dzeusking-dev Date: Mon, 31 Aug 2026 07:46:28 +0200 Subject: [PATCH 2/9] fix(windows): add generic runtime recovery trigger --- scripts/Install-PCFileBridgeAutostart.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/Install-PCFileBridgeAutostart.ps1 b/scripts/Install-PCFileBridgeAutostart.ps1 index 6678989..469447c 100644 --- a/scripts/Install-PCFileBridgeAutostart.ps1 +++ b/scripts/Install-PCFileBridgeAutostart.ps1 @@ -17,9 +17,10 @@ $connectScript = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot 'Connect-PC $account = [Security.Principal.WindowsIdentity]::GetCurrent().Name $arguments = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{0}" -TunnelId "{1}"' -f $connectScript, $TunnelId $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments -WorkingDirectory ([IO.Path]::GetDirectoryName($connectScript)) -$trigger = New-ScheduledTaskTrigger -AtLogOn -User $account +$logonTrigger = New-ScheduledTaskTrigger -AtLogOn -User $account +$watchdogTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 5) $principal = New-ScheduledTaskPrincipal -UserId $account -LogonType Interactive -RunLevel Limited $settings = New-ScheduledTaskSettingsSet -Compatibility Win8 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DontStopOnIdleEnd -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description 'Starts the private OpenAI Secure MCP Tunnel for PC FileBridge after user logon.' | Out-Null +Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger @($logonTrigger, $watchdogTrigger) -Principal $principal -Settings $settings -Description 'Starts the private OpenAI Secure MCP Tunnel for PC FileBridge after user logon and retries stopped runtimes every five minutes.' | Out-Null Write-Output "AUTOSTART_INSTALLED task=$TaskName overwrite=false" From 5e78bc0ba3d0ee7097715b3fc97940d244b608db Mon Sep 17 00:00:00 2001 From: dzeusking-dev Date: Mon, 31 Aug 2026 07:46:30 +0200 Subject: [PATCH 3/9] fix(windows): add role runtime recovery trigger --- scripts/Install-PCFileBridgeRoleAutostart.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/Install-PCFileBridgeRoleAutostart.ps1 b/scripts/Install-PCFileBridgeRoleAutostart.ps1 index 92bbce5..a8d93d6 100644 --- a/scripts/Install-PCFileBridgeRoleAutostart.ps1 +++ b/scripts/Install-PCFileBridgeRoleAutostart.ps1 @@ -38,9 +38,10 @@ if (-not [string]::IsNullOrWhiteSpace($RuntimeLocalAppData)) { } $arguments = $argumentParts -join ' ' $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments -WorkingDirectory ([IO.Path]::GetDirectoryName($taskScript)) -$trigger = New-ScheduledTaskTrigger -AtLogOn -User $account +$logonTrigger = New-ScheduledTaskTrigger -AtLogOn -User $account +$watchdogTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 5) $principal = New-ScheduledTaskPrincipal -UserId $account -LogonType Interactive -RunLevel Limited $settings = New-ScheduledTaskSettingsSet -Compatibility Win8 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DontStopOnIdleEnd -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "Starts the $Role PC FileBridge auto-volume monitor after user logon." | Out-Null +Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger @($logonTrigger, $watchdogTrigger) -Principal $principal -Settings $settings -Description "Starts the $Role PC FileBridge auto-volume monitor after user logon and retries stopped runtimes every five minutes." | Out-Null Write-Output "ROLE_AUTOSTART_INSTALLED task=$TaskName role=$Role overwrite=false" From a9b3e912adc4cb02b90848c237b2a7d13af1b091 Mon Sep 17 00:00:00 2001 From: dzeusking-dev Date: Mon, 31 Aug 2026 07:46:32 +0200 Subject: [PATCH 4/9] test(windows): require recovery triggers --- scripts/tunnel-role-contract.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/tunnel-role-contract.mjs b/scripts/tunnel-role-contract.mjs index c474a16..f2b9f95 100644 --- a/scripts/tunnel-role-contract.mjs +++ b/scripts/tunnel-role-contract.mjs @@ -47,6 +47,12 @@ for (const autostartInstaller of autostartInstallers) { if (/ExecutionTimeLimit\s+\(New-TimeSpan/i.test(autostartInstaller)) { throw new Error("Autostart installer must not impose a finite execution time limit."); } + if (!autostartInstaller.includes("-RepetitionInterval (New-TimeSpan -Minutes 5)")) { + throw new Error("Autostart installer must include the five-minute recovery trigger."); + } + if (!autostartInstaller.includes("-Trigger @($logonTrigger, $watchdogTrigger)")) { + throw new Error("Autostart installer must register both logon and recovery triggers."); + } } const volumePolicy = (rootIdPrefix) => ({ enabled: true, From 36c7f93b375ac99e852646f8960c1ed63ad0ecad Mon Sep 17 00:00:00 2001 From: dzeusking-dev Date: Mon, 31 Aug 2026 07:46:36 +0200 Subject: [PATCH 5/9] docs: document runtime recovery trigger --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55d8b3b..e1259c7 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ For role-bound automatic volume discovery, install the dedicated per-user task: .\scripts\Install-PCFileBridgeRoleAutostart.ps1 -Role pc-local ``` -If the protected tunnel runtime intentionally uses a separate local application-data directory, pass its absolute path with `-RuntimeLocalAppData`. The task script runs a singleton monitor and does not contain a tunnel identifier or API key in its arguments. Both installers create long-running tasks with no finite execution limit and do not stop them on battery or idle transitions; they refuse to replace an existing task. +If the protected tunnel runtime intentionally uses a separate local application-data directory, pass its absolute path with `-RuntimeLocalAppData`. The task script runs a singleton monitor and does not contain a tunnel identifier or API key in its arguments. Both installers create long-running tasks with no finite execution limit, do not stop them on battery or idle transitions, and add a five-minute recovery trigger for externally interrupted runtimes; they refuse to replace an existing task. ## Full-drive mode From 71963f10ba1fb7fd5720dea018afc42f3eef9495 Mon Sep 17 00:00:00 2001 From: dzeusking-dev Date: Mon, 31 Aug 2026 07:46:38 +0200 Subject: [PATCH 6/9] docs: record runtime recovery trigger --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27639c7..d9faff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Fixed Windows autostart durability: the tunnel task now has no finite execution limit and is not stopped by battery or idle transitions. - Added a Windows PowerShell 5.1-compatible role-bound auto-volume task installer with optional isolated runtime state. - Canonicalized role casing before task registration and case-sensitive contract lookup. +- Added a five-minute recovery trigger so externally interrupted runtimes restart without another logon. - Added a regression contract for the required long-running Scheduled Task settings. ## 0.2.1 - 2026-08-30 From 48ba21cebb141f8f107b740a6964d0fc0229fcb9 Mon Sep 17 00:00:00 2001 From: dzeusking-dev Date: Mon, 31 Aug 2026 07:53:17 +0200 Subject: [PATCH 7/9] ci: allow explicit release dispatch for guarded tags --- .github/workflows/release.yml | 64 +++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0002d32..1ec1015 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,14 +4,23 @@ on: push: tags: - "v*" + workflow_dispatch: + inputs: + tag: + description: Existing immutable semantic release tag, for example v0.2.2 + required: true + type: string permissions: contents: read concurrency: - group: release-${{ github.ref }} + group: release-${{ inputs.tag || github.ref }} cancel-in-progress: false +env: + RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + jobs: build: name: Verify and build immutable artifacts @@ -23,6 +32,7 @@ jobs: - name: Check out tagged source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ inputs.tag || github.ref }} fetch-depth: 0 persist-credentials: false @@ -36,19 +46,21 @@ jobs: shell: bash run: | set -euo pipefail + [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] package_version="$(node --input-type=commonjs -p "require('./package.json').version")" expected_tag="v${package_version}" - if [[ "${GITHUB_REF_NAME}" != "${expected_tag}" ]]; then - echo "Release tag ${GITHUB_REF_NAME} does not match package version ${package_version}." >&2 + if [[ "${RELEASE_TAG}" != "${expected_tag}" ]]; then + echo "Release tag ${RELEASE_TAG} does not match package version ${package_version}." >&2 exit 1 fi - if [[ "$(git rev-parse "${GITHUB_REF_NAME}^{commit}")" != "${GITHUB_SHA}" ]]; then + release_sha="$(git rev-parse "${RELEASE_TAG}^{commit}")" + if [[ "${GITHUB_EVENT_NAME}" == "push" && "${release_sha}" != "${GITHUB_SHA}" ]]; then echo "The release tag does not resolve to the workflow commit." >&2 exit 1 fi git fetch --no-tags --force origin main:refs/remotes/origin/main - if ! git merge-base --is-ancestor "${GITHUB_SHA}" refs/remotes/origin/main; then - echo "Tagged commit ${GITHUB_SHA} is not on origin/main." >&2 + if ! git merge-base --is-ancestor "${release_sha}" refs/remotes/origin/main; then + echo "Tagged commit ${release_sha} is not on origin/main." >&2 exit 1 fi @@ -72,17 +84,17 @@ jobs: rm -rf release mkdir release - runtime_name="pc-filebridge-${GITHUB_REF_NAME}-runtime-npm.tgz" - source_name="pc-filebridge-${GITHUB_REF_NAME}-source.tar.gz" - sbom_name="pc-filebridge-${GITHUB_REF_NAME}-sbom.cdx.json" + runtime_name="pc-filebridge-${RELEASE_TAG}-runtime-npm.tgz" + source_name="pc-filebridge-${RELEASE_TAG}-source.tar.gz" + sbom_name="pc-filebridge-${RELEASE_TAG}-sbom.cdx.json" package_file="$(npm pack --ignore-scripts --silent)" mv -- "${package_file}" "release/${runtime_name}" git archive \ --format=tar.gz \ - --prefix="pc-filebridge-${GITHUB_REF_NAME}-source/" \ + --prefix="pc-filebridge-${RELEASE_TAG}-source/" \ --output="release/${source_name}" \ - "${GITHUB_SHA}" + "${release_sha}" npm sbom --sbom-format cyclonedx > "release/${sbom_name}" ( @@ -95,12 +107,12 @@ jobs: run: >- node scripts/verify-release-artifact.mjs --release-dir release - --tag "${GITHUB_REF_NAME}" + --tag "${RELEASE_TAG}" - name: Preserve verified release bundle uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: pc-filebridge-release-${{ github.ref_name }} + name: pc-filebridge-release-${{ env.RELEASE_TAG }} path: release/ if-no-files-found: error retention-days: 90 @@ -120,6 +132,7 @@ jobs: - name: Re-check release identity uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ inputs.tag || github.ref }} fetch-depth: 0 persist-credentials: false @@ -127,16 +140,17 @@ jobs: shell: bash run: | set -euo pipefail + [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] package_version="$(node --input-type=commonjs -p "require('./package.json').version")" - [[ "${GITHUB_REF_NAME}" == "v${package_version}" ]] - [[ "$(git rev-parse "${GITHUB_REF_NAME}^{commit}")" == "${GITHUB_SHA}" ]] + [[ "${RELEASE_TAG}" == "v${package_version}" ]] + release_sha="$(git rev-parse "${RELEASE_TAG}^{commit}")" git fetch --no-tags --force origin main:refs/remotes/origin/main - git merge-base --is-ancestor "${GITHUB_SHA}" refs/remotes/origin/main + git merge-base --is-ancestor "${release_sha}" refs/remotes/origin/main - name: Download verified release bundle uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: pc-filebridge-release-${{ github.ref_name }} + name: pc-filebridge-release-${{ env.RELEASE_TAG }} path: release - name: Re-verify downloaded checksums @@ -152,9 +166,9 @@ jobs: uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: | - release/pc-filebridge-${{ github.ref_name }}-runtime-npm.tgz - release/pc-filebridge-${{ github.ref_name }}-source.tar.gz - release/pc-filebridge-${{ github.ref_name }}-sbom.cdx.json + release/pc-filebridge-${{ env.RELEASE_TAG }}-runtime-npm.tgz + release/pc-filebridge-${{ env.RELEASE_TAG }}-source.tar.gz + release/pc-filebridge-${{ env.RELEASE_TAG }}-sbom.cdx.json release/SHA256SUMS - name: Create GitHub release @@ -163,11 +177,11 @@ jobs: shell: bash run: | set -euo pipefail - gh release create "${GITHUB_REF_NAME}" \ - "release/pc-filebridge-${GITHUB_REF_NAME}-runtime-npm.tgz" \ - "release/pc-filebridge-${GITHUB_REF_NAME}-source.tar.gz" \ - "release/pc-filebridge-${GITHUB_REF_NAME}-sbom.cdx.json" \ + gh release create "${RELEASE_TAG}" \ + "release/pc-filebridge-${RELEASE_TAG}-runtime-npm.tgz" \ + "release/pc-filebridge-${RELEASE_TAG}-source.tar.gz" \ + "release/pc-filebridge-${RELEASE_TAG}-sbom.cdx.json" \ "release/SHA256SUMS" \ --verify-tag \ --generate-notes \ - --title "PC FileBridge ${GITHUB_REF_NAME}" + --title "PC FileBridge ${RELEASE_TAG}" From 692f66f8348f32992f1e5ad0a62741df33d3d663 Mon Sep 17 00:00:00 2001 From: dzeusking-dev Date: Mon, 31 Aug 2026 07:53:40 +0200 Subject: [PATCH 8/9] ci: dispatch verified release after guarded tag creation --- .github/workflows/create-release-tag.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/create-release-tag.yml b/.github/workflows/create-release-tag.yml index 428f643..7bcae39 100644 --- a/.github/workflows/create-release-tag.yml +++ b/.github/workflows/create-release-tag.yml @@ -9,6 +9,7 @@ on: type: string permissions: + actions: write contents: write concurrency: @@ -29,8 +30,9 @@ jobs: ref: main fetch-depth: 0 - - name: Verify and push tag + - name: Verify, push tag, and dispatch verified release env: + GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ inputs.tag }} shell: bash run: | @@ -52,3 +54,4 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git tag -a "${RELEASE_TAG}" "${release_sha}" -m "PC FileBridge ${RELEASE_TAG}" git push origin "refs/tags/${RELEASE_TAG}" + gh workflow run release.yml --ref main -f "tag=${RELEASE_TAG}" From da3af8f9e849476bf3f8449ca6244efc43976b40 Mon Sep 17 00:00:00 2001 From: dzeusking-dev Date: Mon, 31 Aug 2026 07:54:12 +0200 Subject: [PATCH 9/9] ci: resolve release commit within artifact build step --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ec1015..81a4db7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,6 +90,7 @@ jobs: package_file="$(npm pack --ignore-scripts --silent)" mv -- "${package_file}" "release/${runtime_name}" + release_sha="$(git rev-parse "${RELEASE_TAG}^{commit}")" git archive \ --format=tar.gz \ --prefix="pc-filebridge-${RELEASE_TAG}-source/" \