Record v6.1.0 unsigned Windows exception

This commit is contained in:
rcourtman 2026-07-22 15:37:48 +01:00
parent 2c3c188fc8
commit 674364c749
20 changed files with 369 additions and 52 deletions

View file

@ -365,7 +365,7 @@ jobs:
retention-days: 30
build:
name: Build and Validate Signed Candidate
name: Build and Validate Release Candidate
needs: [sign-macos-agent, sign-windows-agent]
if: ${{ always() && (!inputs.require_macos_signing || needs.sign-macos-agent.result == 'success') && (!inputs.require_windows_signing || needs.sign-windows-agent.result == 'success') }}
runs-on: ubuntu-24.04
@ -450,7 +450,7 @@ jobs:
name: windows-signing-evidence-${{ github.sha }}-${{ inputs.version }}
path: release-candidate-manifest
- name: Build signed release candidate
- name: Build release candidate
run: ./scripts/build-release.sh "${{ inputs.version }}"
env:
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}

View file

@ -37,6 +37,15 @@ on:
description: 'Stable only: reason for hotfix soak exception'
required: false
type: string
unsigned_windows_exception:
description: 'v6.1.0 only: publish Windows agents without Authenticode under the recorded owner exception'
required: false
type: boolean
default: false
unsigned_windows_reason:
description: 'v6.1.0 only: owner reason for the unsigned Windows exception'
required: false
type: string
historical_asset_backfill_only:
description: 'Repair an already-published release packet in place without rebuilding binaries'
required: false
@ -82,6 +91,9 @@ jobs:
v5_eos_date: ${{ steps.promotion.outputs.v5_eos_date }}
hotfix_exception: ${{ steps.promotion.outputs.hotfix_exception }}
hotfix_reason: ${{ steps.promotion.outputs.hotfix_reason }}
require_windows_signing: ${{ steps.promotion.outputs.require_windows_signing }}
unsigned_windows_exception: ${{ steps.promotion.outputs.unsigned_windows_exception }}
unsigned_windows_reason: ${{ steps.promotion.outputs.unsigned_windows_reason }}
promotion_mode: ${{ steps.promotion.outputs.promotion_mode }}
is_stable_patch: ${{ steps.promotion.outputs.is_stable_patch }}
historical_asset_backfill_only: ${{ steps.extract.outputs.historical_asset_backfill_only }}
@ -177,6 +189,8 @@ jobs:
V5_EOS_DATE_INPUT: ${{ github.event.inputs.v5_eos_date }}
HOTFIX_EXCEPTION_INPUT: ${{ github.event.inputs.hotfix_exception }}
HOTFIX_REASON_INPUT: ${{ github.event.inputs.hotfix_reason }}
UNSIGNED_WINDOWS_EXCEPTION_INPUT: ${{ github.event.inputs.unsigned_windows_exception }}
UNSIGNED_WINDOWS_REASON_INPUT: ${{ github.event.inputs.unsigned_windows_reason }}
run: |
set -euo pipefail
@ -201,6 +215,14 @@ jobs:
if [ "${HOTFIX_EXCEPTION_INPUT:-false}" = "true" ]; then
HELPER_ARGS+=(--hotfix-exception)
fi
if [ "${UNSIGNED_WINDOWS_EXCEPTION_INPUT:-false}" = "true" ]; then
HELPER_ARGS+=(
--unsigned-windows-exception
--unsigned-windows-reason "${UNSIGNED_WINDOWS_REASON_INPUT:-}"
)
elif [ -n "${UNSIGNED_WINDOWS_REASON_INPUT:-}" ]; then
HELPER_ARGS+=(--unsigned-windows-reason "${UNSIGNED_WINDOWS_REASON_INPUT}")
fi
python3 scripts/release_control/resolve_release_promotion.py "${HELPER_ARGS[@]}" > "$RUNNER_TEMP/promotion-metadata.out"
rm -f "$NOTES_FILE"
@ -223,7 +245,7 @@ jobs:
with:
version: ${{ needs.prepare.outputs.version }}
require_macos_signing: true
require_windows_signing: ${{ needs.prepare.outputs.is_prerelease != 'true' }}
require_windows_signing: ${{ needs.prepare.outputs.require_windows_signing == 'true' }}
windows_signing_backend: signpath
# Frontend checks run in parallel with backend tests
@ -724,7 +746,10 @@ jobs:
--planned-ga-date "${{ needs.prepare.outputs.ga_date }}" \
--planned-v5-eos-date "${{ needs.prepare.outputs.v5_eos_date }}" \
--hotfix-exception "${{ needs.prepare.outputs.hotfix_exception }}" \
--hotfix-reason "${{ needs.prepare.outputs.hotfix_reason }}"
--hotfix-reason "${{ needs.prepare.outputs.hotfix_reason }}" \
--require-windows-signing "${{ needs.prepare.outputs.require_windows_signing }}" \
--unsigned-windows-exception "${{ needs.prepare.outputs.unsigned_windows_exception }}" \
--unsigned-windows-reason "${{ needs.prepare.outputs.unsigned_windows_reason }}"
# The in-app "What's New" banner only fires when the release body has
# a Highlights section (see frontend-modern/src/components/whatsNewModel.ts).

View file

@ -42,6 +42,15 @@ on:
description: 'Stable rehearsal only: reason for hotfix soak exception'
required: false
type: string
unsigned_windows_exception:
description: 'v6.1.0 only: rehearse Windows agents without Authenticode under the recorded owner exception'
required: false
type: boolean
default: false
unsigned_windows_reason:
description: 'v6.1.0 only: owner reason for the unsigned Windows exception'
required: false
type: string
note:
description: 'Optional note/reason for the dry run'
required: false
@ -70,7 +79,7 @@ jobs:
with:
version: ${{ inputs.version }}
require_macos_signing: true
require_windows_signing: ${{ !contains(inputs.version, '-') }}
require_windows_signing: ${{ !contains(inputs.version, '-') && !(inputs.version == '6.1.0' && inputs.unsigned_windows_exception) }}
windows_signing_backend: signpath
dry-run:
@ -92,6 +101,9 @@ jobs:
soak_hours: ${{ steps.rehearsal.outputs.soak_hours }}
hotfix_exception: ${{ steps.rehearsal.outputs.hotfix_exception }}
hotfix_reason: ${{ steps.rehearsal.outputs.hotfix_reason }}
require_windows_signing: ${{ steps.rehearsal.outputs.require_windows_signing }}
unsigned_windows_exception: ${{ steps.rehearsal.outputs.unsigned_windows_exception }}
unsigned_windows_reason: ${{ steps.rehearsal.outputs.unsigned_windows_reason }}
steps:
- name: Validate release ref
@ -131,6 +143,8 @@ jobs:
V5_EOS_DATE_INPUT: ${{ inputs.v5_eos_date }}
HOTFIX_EXCEPTION_INPUT: ${{ inputs.hotfix_exception }}
HOTFIX_REASON_INPUT: ${{ inputs.hotfix_reason }}
UNSIGNED_WINDOWS_EXCEPTION_INPUT: ${{ inputs.unsigned_windows_exception }}
UNSIGNED_WINDOWS_REASON_INPUT: ${{ inputs.unsigned_windows_reason }}
run: |
set -euo pipefail
@ -171,6 +185,14 @@ jobs:
if [ "${HOTFIX_EXCEPTION_INPUT:-false}" = "true" ]; then
HELPER_ARGS+=(--hotfix-exception)
fi
if [ "${UNSIGNED_WINDOWS_EXCEPTION_INPUT:-false}" = "true" ]; then
HELPER_ARGS+=(
--unsigned-windows-exception
--unsigned-windows-reason "${UNSIGNED_WINDOWS_REASON_INPUT:-}"
)
elif [ -n "${UNSIGNED_WINDOWS_REASON_INPUT:-}" ]; then
HELPER_ARGS+=(--unsigned-windows-reason "${UNSIGNED_WINDOWS_REASON_INPUT}")
fi
if [ "${EVENT_NAME}" = "schedule" ] && [ -z "${ROLLBACK_VERSION_INPUT:-}" ]; then
# Scheduled watchdog runs carry no dispatch inputs; derive the
# rollback target instead of failing. Manual dispatches never get
@ -373,6 +395,11 @@ jobs:
if [ -n "${{ steps.rehearsal.outputs.hotfix_reason }}" ]; then
echo "- Hotfix reason: ${{ steps.rehearsal.outputs.hotfix_reason }}"
fi
echo "- Windows Authenticode required: ${{ steps.rehearsal.outputs.require_windows_signing }}"
echo "- Unsigned Windows exception: ${{ steps.rehearsal.outputs.unsigned_windows_exception }}"
if [ -n "${{ steps.rehearsal.outputs.unsigned_windows_reason }}" ]; then
echo "- Unsigned Windows reason: ${{ steps.rehearsal.outputs.unsigned_windows_reason }}"
fi
if [ -n "${NOTE}" ]; then
echo "- Operator note: ${NOTE}"
fi
@ -429,6 +456,9 @@ jobs:
PREFLIGHT_RESULT: ${{ needs.dry-run.result }}
DEMO_RESULT: ${{ needs.demo_path_preflight.result }}
WINDOWS_SIGNING_BACKEND: ${{ needs.build_release_candidate.outputs.windows_signing_backend }}
REQUIRE_WINDOWS_SIGNING: ${{ needs.dry-run.outputs.require_windows_signing }}
UNSIGNED_WINDOWS_EXCEPTION: ${{ needs.dry-run.outputs.unsigned_windows_exception }}
UNSIGNED_WINDOWS_REASON: ${{ needs.dry-run.outputs.unsigned_windows_reason }}
run: |
set -euo pipefail
failed=0
@ -445,7 +475,7 @@ jobs:
require_result "release preflight" "$PREFLIGHT_RESULT" success
require_result "stable demo no-mutation verification" "$DEMO_RESULT" success
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "${VERSION_INPUT:-}" ]; then
require_result "exact-SHA signed candidate" "$CANDIDATE_RESULT" success
require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success
elif [ "$CANDIDATE_RESULT" != "skipped" ]; then
echo "::error::Scheduled watchdog candidate result was ${CANDIDATE_RESULT}; expected skipped."
failed=1
@ -458,7 +488,12 @@ jobs:
echo "- Version: \`${VERSION_INPUT:-scheduled}\`"
echo "- Release preflight: \`${PREFLIGHT_RESULT}\`"
echo "- Stable demo verification: \`${DEMO_RESULT}\`"
echo "- Signed candidate: \`${CANDIDATE_RESULT}\`"
echo "- Release candidate: \`${CANDIDATE_RESULT}\`"
echo "- Windows Authenticode required: \`${REQUIRE_WINDOWS_SIGNING}\`"
echo "- Unsigned Windows exception: \`${UNSIGNED_WINDOWS_EXCEPTION}\`"
if [ -n "${UNSIGNED_WINDOWS_REASON:-}" ]; then
echo "- Unsigned Windows reason: ${UNSIGNED_WINDOWS_REASON}"
fi
if [ -n "${WINDOWS_SIGNING_BACKEND:-}" ]; then
echo "- Windows signing backend: \`${WINDOWS_SIGNING_BACKEND}\`"
fi

View file

@ -1021,18 +1021,23 @@ Companion drill:
6. Confirm `v6.0.5` release timestamps and asset count remain unchanged and
the public demo remains healthy on stable `6.0.5`.
- Pass when:
The external rehearsal proves the canonical candidate builder, local tests
pin candidate-only publication and GitHub digest validation, and the static
release DAG contains no duplicate release build, backend-to-integration
serialization, full-download standard validator, or avoidable post-release
serialization.
The external rehearsal proves the canonical exact-SHA candidate builder and
its version-specific native-signing policy. If the `v6.1.0` Windows exception
is used, the rehearsal must preserve checksum, detached-signature, manifest,
and digest controls and record the owner reason and public disclosure. Local
tests pin candidate-only publication and GitHub digest validation, and the
static release DAG contains no duplicate release build,
backend-to-integration serialization, full-download standard validator, or
avoidable post-release serialization.
- Latest exercised record:
`docs/release-control/v6/internal/records/single-build-release-promotion-path-2026-07-09.md`
- Block release if:
Publication rebuilds release assets, the candidate manifest does not pin the
exact SHA and complete asset set, standard validation downloads the full
release packet, independent post-release jobs are serialized, or the
definitive verdict can pass without all applicable downstream results.
definitive verdict can pass without all applicable downstream results. Also
block if an unsigned stable Windows exception can apply to any version other
than `v6.1.0` or can omit its owner reason and release-note disclosure.
## Gate: `operational-trust-canonical-lifecycle`

View file

@ -174,6 +174,13 @@ Cloud, and self-hosted production users.
Run` and the normal single-build publication workflow. Only a security,
data-loss, upgrade-breaking, startup-blocking, or release-pipeline defect
may reopen the cutoff; ordinary findings move to v6.1.1.
- After exact-SHA rehearsal `29927692302` exposed unavailable external
SignPath configuration, the release owner approved a `v6.1.0`-only
Windows Authenticode exception. The Windows artifacts must remain bound
by the exact-SHA candidate manifest, checksums, detached `.sig`/`.sshsig`
signatures, and published digests, and the public notes must disclose the
Unknown Publisher state. Later stable releases restore the Authenticode
requirement; this flag cannot be reused for another version.
- Unproved self-service commercial transitions remain unavailable and
unadvertised under the exposure-safety gate. This exception does not
authorize enabling that feature or running a production billing proof.
@ -181,11 +188,12 @@ Cloud, and self-hosted production users.
## Single-Build Release Path
1. Every normal RC, stable, and patch release is initiated once through
`create-release.yml`. The workflow builds one signed candidate for the exact
pushed SHA while frontend, backend, Docker, Helm, and integration checks run
in parallel. No tag, draft, or public release mutation occurs until those
checks and the candidate build pass.
2. The signed candidate is uploaded as a one-day Actions artifact with a
`create-release.yml`. The workflow builds one exact-SHA candidate with the
native signing lanes required by that version's governed policy while
frontend, backend, Docker, Helm, and integration checks run in parallel. No
tag, draft, or public release mutation occurs until those checks and the
candidate build pass.
2. The release candidate is uploaded as a one-day Actions artifact with a
machine-readable manifest that pins source SHA, version, filename, size, and
SHA-256 for every release asset. Publication downloads and verifies that
exact candidate; it must not rebuild release binaries or installers.

View file

@ -20,6 +20,21 @@ metadata on top of the content cutoff base. The exact publish SHA must be bound
by the no-public-release `Release Dry Run` and then consumed by the normal
single-build release workflow without further product changes.
## Windows Signing Exception
The first exact-SHA stable rehearsal, GitHub Actions run `29927692302`, stopped
before candidate assembly because the external SignPath configuration was
unavailable. It created no tag, draft, or public release. The release owner
then explicitly approved publishing `v6.1.0` without Windows Authenticode
signing rather than entering another release-candidate loop.
This is a `v6.1.0`-only exception. Windows Unified Agent binaries remain bound
to the exact source SHA and candidate manifest and must retain checksum,
detached `.sig`/`.sshsig`, and post-publication digest verification. The public
release notes must state that the Windows binaries are not
Authenticode-signed and may show an Unknown Publisher warning. Every later
stable version restores the Windows Authenticode requirement automatically.
## Cutoff Reopen Rule
Only a security defect, data-loss risk, broken upgrade, startup failure, or

View file

@ -8396,7 +8396,7 @@
},
{
"id": "single-build-release-promotion-path",
"summary": "Confirm normal RC, stable, and patch releases build one exact-SHA signed candidate in parallel with checks, promote that candidate without rebuilding, validate GitHub asset digests without full packet downloads, and run independent post-publication lanes concurrently.",
"summary": "Confirm normal RC, stable, and patch releases build one exact-SHA candidate with policy-required native signing in parallel with checks, promote that candidate without rebuilding, validate GitHub asset digests without full packet downloads, and run independent post-publication lanes concurrently.",
"owner": "project-owner",
"blocking_level": "release-ready",
"minimum_evidence_tier": "real-external-e2e",

View file

@ -357,10 +357,10 @@ TLS floor in the dynamic config.
1. Add or change deployment-type detection, update planning, or apply behavior through `internal/updates/`
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, artifact release-line validation, the canonical version file, operator-facing release packet content, prerelease feedback intake wording, historical published-release integrity backfill, release asset validation status publication, download endpoint checksum/signature header proof, end-to-end install.sh smoke against the published release, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/mobile_release_gate.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/validate_artifact_release_line.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/install-sh-smoke.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, and `.github/workflows/validate-release-assets.yml`
Normal releases are single-build promotions. The exact pushed SHA must
produce one signed candidate through
`.github/workflows/build-release-candidate.yml` while independent release
checks run in parallel. `create-release.yml` may publish only that candidate
after `scripts/release_candidate_manifest.py` verifies its version, source
produce one release candidate with the policy-required native signing lanes
through `.github/workflows/build-release-candidate.yml` while independent
release checks run in parallel. `create-release.yml` may publish only that
candidate after `scripts/release_candidate_manifest.py` verifies its version, source
SHA, filenames, sizes, and SHA-256 values. Standard post-upload validation
must compare that manifest with GitHub's server-side asset digests instead
of downloading the complete release packet again. Historical repair and
@ -374,17 +374,19 @@ TLS floor in the dynamic config.
it must not decompress a multi-gigabyte release archive again for every
required entry, and the release-promotion contract test must reject a return
to per-entry archive streaming.
A manually dispatched release rehearsal must activate the same signed
A manually dispatched release rehearsal must activate the same
candidate build whenever its required `version` input is non-empty and must
apply the same channel-specific native-signing policy as a publish run.
macOS notarization remains mandatory for both prerelease and stable
candidates. Windows Authenticode remains mandatory for stable candidates;
prerelease candidates may retain checksum and detached-signature
verification without Authenticode while the release packet explicitly
discloses the unknown-publisher warning and stable promotion remains
blocked. A cheap signing-configuration job must report every missing secret
for the platforms required by that candidate before either platform runner
is allocated. Stable Windows signing must use SignPath's GitHub
candidates. Windows Authenticode remains mandatory for stable candidates
except for the explicitly version-bound `v6.1.0` owner exception;
prerelease candidates and that one stable exception may retain checksum and
detached-signature verification without Authenticode while the release
packet explicitly discloses the unknown-publisher warning. Prerelease promotion remains
blocked on the normal stable signing requirement, and every stable version
after `v6.1.0` restores it automatically. A cheap signing-configuration job
must report every missing secret for the platforms required by that
candidate before either platform runner is allocated. Stable Windows signing must use SignPath's GitHub
trusted-build-system action by default, submit an immutable GitHub artifact
by id, verify every returned executable, and retain evidence binding the
request, source SHA, signer identity, and file digests. A repository-secret
@ -1028,9 +1030,12 @@ Authenticode backend. The reusable builder fails fast on missing configuration,
submits the GitHub-hosted unsigned artifact through the pinned official action,
verifies all returned executables, and stores request/source/signer/digest
evidence beside the candidate manifest. Release Dry Run now has a terminal
verdict covering the signed candidate and no-mutation demo lane. The gate stays
blocked until the external SignPath project is configured and one stable dry
run passes for an exact `main` SHA.
verdict covering the exact-SHA candidate and no-mutation demo lane. Stable
rehearsal `29927692302` confirmed that the external SignPath project was not
configured and stopped without creating a public release. The release owner
subsequently approved a `v6.1.0`-only unsigned-Windows exception; the gate stays
blocked until a new exact-`main` rehearsal proves that exception and the
remaining candidate controls.
Every caller of the reusable release-candidate builder must delegate
`actions: read` alongside `contents: read`; the Windows signing job reads the
exact uploaded artifact through the GitHub Actions API, and GitHub validates
@ -1049,7 +1054,7 @@ This stable minor release uses `promoted_from_tag=v6.1.0-rc.4`,
recorded on 2026-07-22. The workflow input `hotfix_exception=true` carries that
approved soak bypass through the existing promotion resolver; it does not
reclassify the release as a patch hotfix. The exact stable `main` SHA must pass
the signed no-publication dry run before the same SHA is dispatched through the
the no-publication dry run before the same SHA is dispatched through the
single-build publish workflow. The release publishes versioned GitHub, Docker,
and Helm artifacts and advances the stable/latest install pointers and stable
semver aliases. It promotes
@ -1080,9 +1085,12 @@ Agent Doctor into a routed, filterable diagnostic workflow with copyable
reports and platform-correct host-local cleanup handoff, adds SAS and SCSI SMART
coverage, restores actionable agent-update controls, fixes Patrol finding and
proposal provenance, improves metrics and audit-store concurrency, and prepares
the stable Windows SignPath signing path. Stable publication requires the
SignPath-returned Windows binaries to pass Authenticode and signer verification;
the unsigned-Windows prerelease exception does not apply to `v6.1.0`.
the stable Windows SignPath signing path. For `v6.1.0`, the release owner
explicitly waived Authenticode after the first stable rehearsal exposed
unavailable external SignPath configuration. The unsigned Windows binaries
remain exact-SHA and manifest-bound with checksum, detached `.sig`/`.sshsig`,
and published-digest verification, and the release notes disclose the Unknown
Publisher state. This exception cannot apply to a later stable version.
The stable server cut is classified `existing-mobile-build-compatible`. Pulse
Mobile `1.0.0` iOS build `11` and Android versionCode `9` remain the existing
candidate builds; the canonical core/mobile contract proves that `v6.1.0`
@ -2465,7 +2473,9 @@ externally owned bounded residual, but only when the RC packet explicitly
discloses the unsigned Windows publisher state and the Windows binaries retain
the exact-SHA candidate, checksum, detached-signature, and post-publication
digest controls. Stable publication and the stable-path dry-run must continue
to require both native signing lanes. `scripts/build-release.sh` must replace
to require both native signing lanes except for the recorded, version-bound
`v6.1.0` Windows exception; every subsequent stable release restores both
requirements. `scripts/build-release.sh` must replace
only the native targets required by those independent inputs and must fail
closed when a required native-binary directory or target is absent.
Historical published-release repair must flow through

View file

@ -41,8 +41,9 @@ plus the final compatibility and reliability fixes in the stable cutoff.
- `--report-ip` can set the Unified Agent's reported host address and becomes
the leading host-identity signal.
- Disk monitoring understands SAS transport and SCSI SMART attributes.
- Windows Unified Agent artifacts use the governed exact-SHA SignPath path;
stable publication requires Authenticode, signer, and digest verification.
- Windows Unified Agent artifacts remain bound to the exact release SHA and
retain checksum, detached-signature, manifest, and published-digest
verification.
## Improved
@ -93,6 +94,11 @@ Use the normal v6 install or update flow for `v6.1.0`. Existing Unified Agent
configurations keep one authoritative Pulse server; observer reporting remains
opt-in and no observer is created during upgrade.
Windows Unified Agent binaries in `v6.1.0` are not Authenticode-signed and may
show an Unknown Publisher warning. Verify the published checksums and detached
`.sig` or `.sshsig` signatures before installation. This is a one-release owner
exception; later stable releases restore the Windows Authenticode requirement.
The rollback target is `v6.0.5`. The exact rollback reinstall command is:
```bash

View file

@ -17,8 +17,8 @@ _This changelog describes stable `v6.1.0` compared with stable `v6.0.5`._
bounded native-tool execution.
- Routed Agent Doctor workflow, `--report-ip` host identity, SAS transport, and
SCSI SMART attribute support.
- Exact-SHA SignPath submission, Authenticode verification, signer validation,
and evidence recording for stable Windows Unified Agent artifacts.
- Exact-SHA, checksum, detached-signature, manifest, and published-digest
verification for Windows Unified Agent artifacts.
## Changed
@ -60,7 +60,11 @@ _This changelog describes stable `v6.1.0` compared with stable `v6.0.5`._
- Rollback target: `v6.0.5`
- Rollback command: `./scripts/install.sh --version v6.0.5`
- Promotion path: owner-approved exact-SHA stable cutoff from `main`, using the
one-version soak exception and the single-build signed-candidate workflow
one-version soak exception and the single-build release-candidate workflow
- Windows signing decision: one-release owner exception; the `v6.1.0` Windows
Unified Agent binaries are not Authenticode-signed and may show an Unknown
Publisher warning, while checksums and detached `.sig`/`.sshsig` signatures
remain required. Later stable releases restore the Authenticode requirement.
- Mobile decision: `existing-mobile-build-compatible`; Pulse Mobile `1.0.0`
iOS build `11` and Android versionCode `9` require no companion upload, and
no public store rollout is part of this server release

View file

@ -345,7 +345,7 @@ func TestCurrentStableMinorReleasePacketTracksInstallMetadata(t *testing.T) {
"Operational Trust",
"Actions provides a dedicated inbox",
"existing Pulse Mobile candidate",
"SignPath",
"not Authenticode-signed",
"The rollback target is `v"+previous+"`",
)
assertFileContainsAllNormalized(t, changelogPath,
@ -383,9 +383,9 @@ func TestCurrentStableMinorReleasePacketTracksInstallMetadata(t *testing.T) {
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
"The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
"`rollback_version=v"+previous+"`",
"The exact stable `main` SHA must pass the signed no-publication dry run before the same SHA is dispatched through the single-build publish workflow.",
"The exact stable `main` SHA must pass the no-publication dry run before the same SHA is dispatched through the single-build publish workflow.",
"The stable server cut is classified `existing-mobile-build-compatible`.",
"Stable publication requires the SignPath-returned Windows binaries to pass Authenticode and signer verification",
"This exception cannot apply to a later stable version",
"For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`",
)
}
@ -842,14 +842,23 @@ func TestReleaseCandidateRequiresPlatformNativeAgentSigning(t *testing.T) {
}
assertFileContainsAll(t, repoFile(".github", "workflows", "create-release.yml"),
`require_macos_signing: true`,
`require_windows_signing: ${{ needs.prepare.outputs.is_prerelease != 'true' }}`,
`require_windows_signing: ${{ needs.prepare.outputs.require_windows_signing == 'true' }}`,
`unsigned_windows_exception:`,
`unsigned_windows_reason:`,
`windows_signing_backend: signpath`,
)
assertFileContainsAll(t, repoFile(".github", "workflows", "release-dry-run.yml"),
`Definitive Dry-Run Verdict`,
`require_result "exact-SHA signed candidate" "$CANDIDATE_RESULT" success`,
`require_windows_signing: ${{ !contains(inputs.version, '-') && !(inputs.version == '6.1.0' && inputs.unsigned_windows_exception) }}`,
`require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success`,
`require_result "stable demo no-mutation verification" "$DEMO_RESULT" success`,
)
assertFileContainsAll(t, repoFile("scripts", "release_control", "resolve_release_promotion.py"),
`version != "6.1.0"`,
`unsigned_windows_reason is required`,
`not Authenticode-signed`,
`require_windows_signing = not is_prerelease and not unsigned_windows_exception`,
)
assertFileContainsAll(t, repoFile("scripts", "build-release.sh"),
`PULSE_AGENT_NATIVE_BINARIES_DIR`,
`native_targets=()`,

View file

@ -222,6 +222,9 @@ def render_record(
published_eos = v5_eos_date or summary_metadata.get("planned_v5_eos_date", "")
hotfix_exception = summary_metadata.get("hotfix_exception", "")
hotfix_reason = summary_metadata.get("hotfix_reason", "")
require_windows_signing = summary_metadata.get("windows_authenticode_required", "")
unsigned_windows_exception = summary_metadata.get("unsigned_windows_exception", "")
unsigned_windows_reason = summary_metadata.get("unsigned_windows_reason", "")
operator_note = summary_metadata.get("operator_note", "")
lines = [
@ -248,6 +251,12 @@ def render_record(
lines.append(f"- Hotfix exception: {hotfix_exception}")
if hotfix_reason:
lines.append(f"- Hotfix reason: {hotfix_reason}")
if require_windows_signing:
lines.append(f"- Windows Authenticode required: {require_windows_signing}")
if unsigned_windows_exception:
lines.append(f"- Unsigned Windows exception: {unsigned_windows_exception}")
if unsigned_windows_reason:
lines.append(f"- Unsigned Windows reason: {unsigned_windows_reason}")
if operator_note:
lines.append(f"- Workflow operator note: {operator_note}")
for note in notes:

View file

@ -29,6 +29,9 @@ SUMMARY = """# Prerelease-to-GA Rehearsal Summary
- Planned GA date: 2026-03-15
- Planned v5 end-of-support date: 2026-06-11
- Hotfix exception: false
- Windows Authenticode required: false
- Unsigned Windows exception: true
- Unsigned Windows reason: v6.1.0 owner exception
- Operator note: dry-run from test
"""
@ -43,6 +46,8 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
self.assertEqual(parsed["rollback_command"], "`./scripts/install.sh --version v5.1.23`")
self.assertEqual(parsed["planned_ga_date"], "2026-03-15")
self.assertEqual(parsed["planned_v5_eos_date"], "2026-06-11")
self.assertEqual(parsed["windows_authenticode_required"], "false")
self.assertEqual(parsed["unsigned_windows_exception"], "true")
def test_render_record_contains_required_fields(self) -> None:
rendered = mod.render_record(
@ -65,6 +70,9 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
self.assertIn("Exact rollback or reinstall command: `./scripts/install.sh --version v5.1.23`", rendered)
self.assertIn("Exact GA date to publish: 2026-03-15", rendered)
self.assertIn("Exact v5 end-of-support date to publish: 2026-06-13", rendered)
self.assertIn("Windows Authenticode required: false", rendered)
self.assertIn("Unsigned Windows exception: true", rendered)
self.assertIn("Unsigned Windows reason: v6.1.0 owner exception", rendered)
self.assertIn("Publish GA release notes with the EOS date.", rendered)
self.assertIn("```md", rendered)

View file

@ -487,7 +487,12 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("build_release_candidate:", workflow)
self.assertIn("if: ${{ inputs.version != '' }}", workflow)
self.assertIn("require_macos_signing: true", workflow)
self.assertIn("require_windows_signing: ${{ !contains(inputs.version, '-') }}", workflow)
self.assertIn(
"require_windows_signing: ${{ !contains(inputs.version, '-') && !(inputs.version == '6.1.0' && inputs.unsigned_windows_exception) }}",
workflow,
)
self.assertIn("unsigned_windows_exception:", workflow)
self.assertIn("unsigned_windows_reason:", workflow)
self.assertIn("windows_signing_backend: signpath", workflow)
self.assertRegex(
workflow,
@ -500,7 +505,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
r" actions: read\n contents: read\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
)
self.assertIn("Definitive Dry-Run Verdict", workflow)
self.assertIn('require_result "exact-SHA signed candidate" "$CANDIDATE_RESULT" success', workflow)
self.assertIn('require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success', workflow)
self.assertIn('require_result "stable demo no-mutation verification" "$DEMO_RESULT" success', workflow)
self.assertNotIn("if: ${{ github.event_name == 'workflow_dispatch' }}", workflow)
self.assertIn("record_rc_to_ga_rehearsal.py --run-id ${{ github.run_id }}", workflow)
@ -661,7 +666,11 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("timeout-minutes: 60", candidate_workflow)
self.assertIn("Verify Native Signing Configuration", candidate_workflow)
self.assertEqual(candidate_workflow.count("needs: signing-configuration"), 2)
self.assertIn("require_windows_signing: ${{ needs.prepare.outputs.is_prerelease != 'true' }}", content)
self.assertIn("require_windows_signing: ${{ needs.prepare.outputs.require_windows_signing == 'true' }}", content)
self.assertIn("unsigned_windows_exception:", content)
self.assertIn("unsigned_windows_reason:", content)
self.assertIn('version != "6.1.0"', resolver)
self.assertIn("not Authenticode-signed", resolver)
self.assertIn("windows_signing_backend: signpath", content)
self.assertIn('if [[ "$REQUIRE_WINDOWS_SIGNING" == "true" ]]', candidate_workflow)
self.assertIn("inputs.require_windows_signing", candidate_workflow)

View file

@ -219,6 +219,10 @@ def build_promotion_metadata_section(args: argparse.Namespace) -> str:
lines.append(f"- Hotfix exception: {args.hotfix_exception}")
if args.hotfix_reason:
lines.append(f"- Hotfix reason: {args.hotfix_reason}")
lines.append(f"- Windows Authenticode required: {args.require_windows_signing}")
lines.append(f"- Unsigned Windows exception: {args.unsigned_windows_exception}")
if args.unsigned_windows_reason:
lines.append(f"- Unsigned Windows reason: {args.unsigned_windows_reason}")
return "\n".join(lines)
@ -239,6 +243,9 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--planned-v5-eos-date", default="")
parser.add_argument("--hotfix-exception")
parser.add_argument("--hotfix-reason", default="")
parser.add_argument("--require-windows-signing")
parser.add_argument("--unsigned-windows-exception")
parser.add_argument("--unsigned-windows-reason", default="")
args = parser.parse_args()
if args.validate_notes_file or args.validate_body_file:
@ -264,6 +271,8 @@ def parse_args() -> argparse.Namespace:
"--rollback-target": args.rollback_target,
"--rollback-command": args.rollback_command,
"--hotfix-exception": args.hotfix_exception,
"--require-windows-signing": args.require_windows_signing,
"--unsigned-windows-exception": args.unsigned_windows_exception,
}
missing = [name for name, value in required_render_args.items() if value is None]
if missing:

View file

@ -91,6 +91,9 @@ Old metadata section.
"planned_v5_eos_date": "",
"hotfix_exception": "false",
"hotfix_reason": "",
"require_windows_signing": "false",
"unsigned_windows_exception": "false",
"unsigned_windows_reason": "",
},
)()
@ -113,6 +116,8 @@ Old metadata section.
)
self.assertIn("https://pulserelay.pro/download.html", body)
self.assertIn("- Rollback target: v5.1.28", body)
self.assertIn("- Windows Authenticode required: false", body)
self.assertIn("- Unsigned Windows exception: false", body)
render_release_body.validate_release_body_shape(body, "6.0.0-rc.2")
def test_flattened_release_notes_fail_closed(self) -> None:

View file

@ -221,6 +221,8 @@ def resolve_metadata(
hotfix_exception: bool,
hotfix_reason_input: str,
release_notes_input: str,
unsigned_windows_exception: bool = False,
unsigned_windows_reason_input: str = "",
derive_rollback_when_missing: bool = False,
list_stable_tags_fn: Callable[[], list[str]] = list_stable_tags,
list_same_version_rc_tags_fn: Callable[[str], list[str]] = list_same_version_rc_tags,
@ -236,11 +238,33 @@ def resolve_metadata(
ga_date = (ga_date_input or "").strip()
v5_eos_date = (v5_eos_date_input or "").strip()
hotfix_reason = normalize_whitespace(hotfix_reason_input)
unsigned_windows_reason = normalize_whitespace(unsigned_windows_reason_input)
release_notes = release_notes_input or ""
is_prerelease = is_prerelease_version(version)
stable_patch = is_stable_patch_version(version)
promotion_mode = "prerelease" if is_prerelease else "stable-rc-promotion"
if unsigned_windows_exception:
if version != "6.1.0":
raise ValueError(
"unsigned_windows_exception is approved only for stable v6.1.0. "
"Later stable releases must restore Windows Authenticode signing."
)
if not unsigned_windows_reason:
raise ValueError(
"unsigned_windows_reason is required when unsigned_windows_exception is true."
)
if release_notes and "not authenticode-signed" not in release_notes.lower():
raise ValueError(
"Stable v6.1.0 release_notes must disclose that Windows binaries are not Authenticode-signed."
)
elif unsigned_windows_reason:
raise ValueError(
"unsigned_windows_reason is allowed only when unsigned_windows_exception is true."
)
require_windows_signing = not is_prerelease and not unsigned_windows_exception
if not rollback_tag and derive_rollback_when_missing:
rollback_tag = derive_latest_stable_rollback_tag(version, list_stable_tags_fn())
if not rollback_tag:
@ -374,6 +398,9 @@ def resolve_metadata(
"v5_eos_date": v5_eos_date,
"hotfix_exception": "true" if hotfix_exception else "false",
"hotfix_reason": hotfix_reason,
"unsigned_windows_exception": "true" if unsigned_windows_exception else "false",
"unsigned_windows_reason": unsigned_windows_reason,
"require_windows_signing": "true" if require_windows_signing else "false",
"soak_hours": soak_hours,
}
@ -395,6 +422,8 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--v5-eos-date", default="")
parser.add_argument("--hotfix-exception", action="store_true")
parser.add_argument("--hotfix-reason", default="")
parser.add_argument("--unsigned-windows-exception", action="store_true")
parser.add_argument("--unsigned-windows-reason", default="")
parser.add_argument("--release-notes-file", default="")
return parser.parse_args()
@ -415,6 +444,8 @@ def main() -> int:
hotfix_exception=args.hotfix_exception,
hotfix_reason_input=args.hotfix_reason,
release_notes_input=release_notes,
unsigned_windows_exception=args.unsigned_windows_exception,
unsigned_windows_reason_input=args.unsigned_windows_reason,
)
for key, value in metadata.items():

View file

@ -138,6 +138,83 @@ class ResolveReleasePromotionTest(unittest.TestCase):
)
self.assertEqual(metadata["promoted_from_tag"], "v6.0.0-rc.2")
self.assertEqual(metadata["soak_hours"], "73")
self.assertEqual(metadata["require_windows_signing"], "true")
self.assertEqual(metadata["unsigned_windows_exception"], "false")
def test_v610_owner_exception_allows_disclosed_unsigned_windows_candidate(self) -> None:
metadata = resolver.resolve_metadata(
version="6.1.0",
promoted_from_tag_input="v6.1.0-rc.4",
rollback_version_input="v6.0.5",
ga_date_input="",
v5_eos_date_input="",
hotfix_exception=True,
hotfix_reason_input="Owner waived the remaining prerelease soak.",
release_notes_input=(
"Windows Unified Agent binaries are not Authenticode-signed for v6.1.0."
),
unsigned_windows_exception=True,
unsigned_windows_reason_input=(
"Release owner accepted the Windows unknown-publisher warning for v6.1.0."
),
tag_exists_fn=lambda tag: tag in {"v6.1.0-rc.4", "v6.0.5"},
tag_commit_fn=lambda tag: "rc4-commit",
head_descends_from_fn=lambda commit: commit == "rc4-commit",
tag_created_unix_fn=lambda tag: 100,
now_unix_fn=lambda: 100 + (27 * 3600),
)
self.assertEqual(metadata["require_windows_signing"], "false")
self.assertEqual(metadata["unsigned_windows_exception"], "true")
self.assertEqual(
metadata["unsigned_windows_reason"],
"Release owner accepted the Windows unknown-publisher warning for v6.1.0.",
)
def test_unsigned_windows_exception_is_rejected_for_other_stable_versions(self) -> None:
with self.assertRaisesRegex(ValueError, "approved only for stable v6.1.0"):
resolver.resolve_metadata(
version="6.1.1",
promoted_from_tag_input="",
rollback_version_input="v6.1.0",
ga_date_input="",
v5_eos_date_input="",
hotfix_exception=True,
hotfix_reason_input="Emergency patch.",
release_notes_input="Windows binaries are not Authenticode-signed.",
unsigned_windows_exception=True,
unsigned_windows_reason_input="Not approved for this version.",
tag_exists_fn=lambda tag: True,
)
def test_unsigned_windows_exception_requires_reason_and_release_note_disclosure(self) -> None:
common = {
"version": "6.1.0",
"promoted_from_tag_input": "v6.1.0-rc.4",
"rollback_version_input": "v6.0.5",
"ga_date_input": "",
"v5_eos_date_input": "",
"hotfix_exception": True,
"hotfix_reason_input": "Owner waived the remaining prerelease soak.",
"unsigned_windows_exception": True,
"tag_exists_fn": lambda tag: True,
"tag_commit_fn": lambda tag: "rc4-commit",
"head_descends_from_fn": lambda commit: True,
"tag_created_unix_fn": lambda tag: 100,
"now_unix_fn": lambda: 100 + (27 * 3600),
}
with self.assertRaisesRegex(ValueError, "unsigned_windows_reason is required"):
resolver.resolve_metadata(
**common,
release_notes_input="Windows binaries are not Authenticode-signed.",
unsigned_windows_reason_input="",
)
with self.assertRaisesRegex(ValueError, "must disclose"):
resolver.resolve_metadata(
**common,
release_notes_input="Windows agent details omitted.",
unsigned_windows_reason_input="Owner accepted the warning.",
)
def test_stable_requires_release_notes_notice_when_supplied(self) -> None:
with self.assertRaisesRegex(

View file

@ -82,6 +82,8 @@ python3 scripts/check-workflow-dispatch-inputs.py \
--require v5_eos_date \
--require hotfix_exception \
--require hotfix_reason \
--require unsigned_windows_exception \
--require unsigned_windows_reason \
--require note \
--require mobile_release_decision \
--require mobile_release_evidence
@ -114,6 +116,8 @@ GA_DATE=""
V5_EOS_DATE=""
HOTFIX_EXCEPTION="false"
HOTFIX_REASON=""
UNSIGNED_WINDOWS_EXCEPTION="false"
UNSIGNED_WINDOWS_REASON=""
echo ""
read -r -p "Rollback stable version (for example 5.1.14 or v5.1.14): " ROLLBACK_VERSION
@ -166,6 +170,19 @@ if [ "$IS_PRERELEASE" != "true" ]; then
exit 1
fi
fi
if [ "$VERSION" = "6.1.0" ]; then
echo ""
read -r -p "Use the recorded v6.1.0 unsigned Windows exception? [y/N] " UNSIGNED_WINDOWS_REPLY
if [[ "$UNSIGNED_WINDOWS_REPLY" =~ ^[Yy]$ ]]; then
UNSIGNED_WINDOWS_EXCEPTION="true"
read -r -p "Unsigned Windows exception reason: " UNSIGNED_WINDOWS_REASON
if [ -z "$UNSIGNED_WINDOWS_REASON" ]; then
echo "❌ Error: an owner reason is required for the unsigned Windows exception"
exit 1
fi
fi
fi
fi
echo ""
@ -186,6 +203,12 @@ fi
if [ "$HOTFIX_EXCEPTION" = "true" ]; then
RESOLVER_ARGS+=(--hotfix-exception --hotfix-reason "$HOTFIX_REASON")
fi
if [ "$UNSIGNED_WINDOWS_EXCEPTION" = "true" ]; then
RESOLVER_ARGS+=(
--unsigned-windows-exception
--unsigned-windows-reason "$UNSIGNED_WINDOWS_REASON"
)
fi
python3 scripts/release_control/resolve_release_promotion.py "${RESOLVER_ARGS[@]}" >/tmp/pulse-release-dry-run-metadata.out
cat /tmp/pulse-release-dry-run-metadata.out
@ -200,6 +223,8 @@ gh workflow run release-dry-run.yml \
-f v5_eos_date="$V5_EOS_DATE" \
-f hotfix_exception="$HOTFIX_EXCEPTION" \
-f hotfix_reason="$HOTFIX_REASON" \
-f unsigned_windows_exception="$UNSIGNED_WINDOWS_EXCEPTION" \
-f unsigned_windows_reason="$UNSIGNED_WINDOWS_REASON" \
-f note="Governed release rehearsal for ${VERSION}" \
-f mobile_release_decision="$MOBILE_RELEASE_DECISION" \
-f mobile_release_evidence="$MOBILE_RELEASE_EVIDENCE"

View file

@ -95,6 +95,8 @@ python3 scripts/check-workflow-dispatch-inputs.py \
--require v5_eos_date \
--require hotfix_exception \
--require hotfix_reason \
--require unsigned_windows_exception \
--require unsigned_windows_reason \
--require draft_only \
--require mobile_release_decision \
--require mobile_release_evidence
@ -195,6 +197,8 @@ GA_DATE=""
V5_EOS_DATE=""
HOTFIX_EXCEPTION="false"
HOTFIX_REASON=""
UNSIGNED_WINDOWS_EXCEPTION="false"
UNSIGNED_WINDOWS_REASON=""
echo ""
read -r -p "Rollback stable version (for example 5.1.14 or v5.1.14): " ROLLBACK_VERSION
@ -256,6 +260,19 @@ if [ "$IS_PRERELEASE" != "true" ]; then
exit 1
fi
fi
if [ "$VERSION" = "6.1.0" ]; then
echo ""
read -r -p "Use the recorded v6.1.0 unsigned Windows exception? [y/N] " UNSIGNED_WINDOWS_REPLY
if [[ "$UNSIGNED_WINDOWS_REPLY" =~ ^[Yy]$ ]]; then
UNSIGNED_WINDOWS_EXCEPTION="true"
read -r -p "Unsigned Windows exception reason: " UNSIGNED_WINDOWS_REASON
if [ -z "$UNSIGNED_WINDOWS_REASON" ]; then
echo "❌ Error: an owner reason is required for the unsigned Windows exception"
exit 1
fi
fi
fi
fi
# Trigger the workflow
@ -279,6 +296,12 @@ fi
if [ "$HOTFIX_EXCEPTION" = "true" ]; then
RESOLVER_ARGS+=(--hotfix-exception)
fi
if [ "$UNSIGNED_WINDOWS_EXCEPTION" = "true" ]; then
RESOLVER_ARGS+=(
--unsigned-windows-exception
--unsigned-windows-reason "$UNSIGNED_WINDOWS_REASON"
)
fi
python3 scripts/release_control/resolve_release_promotion.py "${RESOLVER_ARGS[@]}" >/tmp/pulse-release-metadata.out
cat /tmp/pulse-release-metadata.out
@ -294,6 +317,8 @@ if [ -n "$NOTES_FILE" ]; then
--arg v5_eos_date "$V5_EOS_DATE" \
--argjson hotfix_exception "$HOTFIX_EXCEPTION" \
--arg hotfix_reason "$HOTFIX_REASON" \
--argjson unsigned_windows_exception "$UNSIGNED_WINDOWS_EXCEPTION" \
--arg unsigned_windows_reason "$UNSIGNED_WINDOWS_REASON" \
--argjson draft_only false \
--arg mobile_release_decision "$MOBILE_RELEASE_DECISION" \
--arg mobile_release_evidence "$MOBILE_RELEASE_EVIDENCE" \
@ -306,6 +331,8 @@ if [ -n "$NOTES_FILE" ]; then
v5_eos_date: $v5_eos_date,
hotfix_exception: $hotfix_exception,
hotfix_reason: $hotfix_reason,
unsigned_windows_exception: $unsigned_windows_exception,
unsigned_windows_reason: $unsigned_windows_reason,
draft_only: $draft_only,
mobile_release_decision: $mobile_release_decision,
mobile_release_evidence: $mobile_release_evidence