mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Keep published release validation non-destructive
This commit is contained in:
@@ -137,7 +137,7 @@ jobs:
|
||||
--validate-body-file "$RELEASE_BODY_FILE"
|
||||
|
||||
- name: Quarantine malformed release body
|
||||
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'failure'
|
||||
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true' && steps.body_integrity.outcome == 'failure'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -365,7 +365,7 @@ jobs:
|
||||
}'
|
||||
|
||||
- name: Update release body - Success
|
||||
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && steps.validate.outputs.validation_passed == 'true'
|
||||
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true' && steps.body_integrity.outcome == 'success' && steps.validate.outputs.validation_passed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -472,37 +472,12 @@ jobs:
|
||||
}'
|
||||
|
||||
- name: Delete all release assets on failure
|
||||
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
||||
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "❌ Validation failed - deleting all release assets"
|
||||
|
||||
INITIAL_STATE="${{ steps.context.outputs.draft }}"
|
||||
if [ "$INITIAL_STATE" != "true" ]; then
|
||||
echo "Release was published; reverting to draft before deleting assets..."
|
||||
RELEASE_JSON=$(curl --fail-with-body --silent --show-error -X PATCH \
|
||||
-H "Authorization: token $GH_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
||||
-d "$(jq -n \
|
||||
--arg tag "${{ steps.context.outputs.tag }}" \
|
||||
--arg target_commitish "${{ steps.context.outputs.target_commitish }}" \
|
||||
'{draft: true, tag_name: $tag, target_commitish: $target_commitish}')")
|
||||
ACTUAL_RELEASE_TAG=$(echo "$RELEASE_JSON" | jq -r '.tag_name // empty')
|
||||
ACTUAL_TARGET_COMMITISH=$(echo "$RELEASE_JSON" | jq -r '.target_commitish // empty')
|
||||
if [ "$ACTUAL_RELEASE_TAG" != "${{ steps.context.outputs.tag }}" ]; then
|
||||
echo "::error::Validation failure draft update detached release tag ${ACTUAL_RELEASE_TAG}; expected ${{ steps.context.outputs.tag }}."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ACTUAL_TARGET_COMMITISH" != "${{ steps.context.outputs.target_commitish }}" ]; then
|
||||
echo "::error::Validation failure draft update changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${{ steps.context.outputs.target_commitish }}."
|
||||
exit 1
|
||||
fi
|
||||
echo "Release marked as draft to block downloads during remediation."
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
ASSET_IDS=$(curl -s -H "Authorization: token $GH_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}/assets" \
|
||||
| jq -r '.[].id')
|
||||
@@ -522,23 +497,16 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Update release body - Failure
|
||||
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
||||
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "❌ Validation failed - updating release description"
|
||||
|
||||
INITIAL_STATE="${{ steps.context.outputs.draft }}"
|
||||
if [ "$INITIAL_STATE" = "true" ]; then
|
||||
HEADER_LINE="## ❌ Release Asset Validation: FAILED"
|
||||
STATUS_LINE="**Status**: ⛔ Draft release blocked until validation passes"
|
||||
INTRO_LINE="Release assets failed validation checks. All assets have been deleted to prevent publishing an invalid release."
|
||||
else
|
||||
HEADER_LINE="## ❌ Release Asset Validation (Post-Publish): FAILED"
|
||||
STATUS_LINE="**Status**: ⛔ Release reverted to draft while assets are rebuilt"
|
||||
INTRO_LINE="A published release edit introduced invalid assets. The release has been reverted to draft and assets were deleted to stop further downloads."
|
||||
fi
|
||||
HEADER_LINE="## ❌ Release Asset Validation: FAILED"
|
||||
STATUS_LINE="**Status**: ⛔ Draft release blocked until validation passes"
|
||||
INTRO_LINE="Release assets failed validation checks. All assets have been deleted to prevent publishing an invalid release."
|
||||
|
||||
CURRENT_RELEASE_JSON=$(mktemp)
|
||||
CURRENT_BODY_FILE=$(mktemp)
|
||||
@@ -638,11 +606,11 @@ jobs:
|
||||
- name: Fail the workflow
|
||||
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
||||
run: |
|
||||
echo "::error::Release asset validation failed. All assets have been deleted."
|
||||
echo "::error::Release asset validation failed. Draft assets are deleted; published releases remain immutable for explicit remediation."
|
||||
exit 1
|
||||
|
||||
- name: Fail malformed release body
|
||||
if: always() && steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'failure'
|
||||
run: |
|
||||
echo "::error::Release body integrity failed. The release was quarantined as a draft without deleting its assets."
|
||||
echo "::error::Release body integrity failed. Draft releases are quarantined; published releases remain immutable for explicit remediation."
|
||||
exit 1
|
||||
|
||||
@@ -401,6 +401,8 @@ COPY --from=release_payload /amd64/bin/pulse-agent-* /opt/pulse/bin/
|
||||
COPY --from=release_payload /amd64/bin/pulse /opt/pulse/bin/pulse-linux-amd64
|
||||
COPY --from=release_payload /arm64/bin/pulse /opt/pulse/bin/pulse-linux-arm64
|
||||
RUN chmod 755 /opt/pulse/scripts/*.sh /opt/pulse/scripts/*.ps1 && \
|
||||
find /opt/pulse/bin -maxdepth 1 -type f -name 'pulse-agent-*' \
|
||||
! -name '*.sig' ! -name '*.sshsig' -exec chmod 755 {} + && \
|
||||
if [ "$TARGETARCH" = "arm64" ]; then \
|
||||
ln -sf pulse-linux-arm64 /opt/pulse/bin/pulse; \
|
||||
ln -sf /opt/pulse/bin/pulse-agent-linux-arm64 /usr/local/bin/pulse-agent; \
|
||||
|
||||
@@ -110,6 +110,15 @@ container. The promotion resolver, manual release trigger, rendered release
|
||||
body, current release packet, and shipped upgrade guide must agree on those
|
||||
deployment-specific paths.
|
||||
|
||||
Images assembled from immutable release payload contexts must restore
|
||||
executable mode on copied Unified Agent binaries before creating the
|
||||
architecture-resolved `/usr/local/bin/pulse-agent` link. Detached signature
|
||||
sidecars remain non-executable. Release validation may delete invalid assets
|
||||
and rewrite validation annotations only while a release is still a draft. A
|
||||
post-publication edit is observation, not authority to mutate or destroy an
|
||||
immutable release; failed revalidation records a failing status and requires an
|
||||
explicit corrective release path.
|
||||
|
||||
The accelerated exact-SHA release worker must preserve release-gate fidelity
|
||||
under its own resource envelope. Bounded frontend static checks and integration
|
||||
image preparation may overlap, but the full frontend test suite and the
|
||||
|
||||
@@ -202,6 +202,7 @@ func TestReleaseContainerTargetsConsumeImmutableCandidate(t *testing.T) {
|
||||
"FROM prebuilt-runtime-base AS runtime_prebuilt",
|
||||
"COPY --from=release_payload /amd64/bin/pulse /opt/pulse/bin/pulse-linux-amd64",
|
||||
"COPY --from=release_payload /arm64/bin/pulse /opt/pulse/bin/pulse-linux-arm64",
|
||||
"! -name '*.sig' ! -name '*.sshsig' -exec chmod 755 {} +",
|
||||
"FROM alpine:3.20@sha256:",
|
||||
"AS agent_runtime_prebuilt",
|
||||
} {
|
||||
@@ -716,13 +717,27 @@ func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) {
|
||||
`--validate-body-file "$RELEASE_BODY_FILE"`,
|
||||
`--expected-body-file "$CLEAN_BODY_FILE"`,
|
||||
`Quarantine malformed release body`,
|
||||
`The release was quarantined as a draft without deleting its assets.`,
|
||||
`Draft releases are quarantined; published releases remain immutable for explicit remediation.`,
|
||||
`name: Update release body - Success
|
||||
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true'`,
|
||||
`name: Delete all release assets on failure
|
||||
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true'`,
|
||||
`name: Update release body - Failure
|
||||
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true'`,
|
||||
}
|
||||
for _, needle := range validationRequired {
|
||||
if !strings.Contains(validationWorkflow, needle) {
|
||||
t.Fatalf("validate-release-assets.yml missing required status publication contract: %s", needle)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{
|
||||
"Release was published; reverting to draft before deleting assets",
|
||||
"A published release edit introduced invalid assets",
|
||||
} {
|
||||
if strings.Contains(validationWorkflow, forbidden) {
|
||||
t.Fatalf("published release validation must not retain mutation path %q", forbidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentStablePatchReleasePacketTracksInstallMetadata(t *testing.T) {
|
||||
|
||||
@@ -1484,7 +1484,27 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn("--expected-body-file \"$CLEAN_BODY_FILE\"", validation_workflow)
|
||||
self.assertIn("Quarantine malformed release body", validation_workflow)
|
||||
self.assertIn(
|
||||
"The release was quarantined as a draft without deleting its assets.",
|
||||
"Draft releases are quarantined; published releases remain immutable for explicit remediation.",
|
||||
validation_workflow,
|
||||
)
|
||||
for step in (
|
||||
"Quarantine malformed release body",
|
||||
"Update release body - Success",
|
||||
"Delete all release assets on failure",
|
||||
"Update release body - Failure",
|
||||
):
|
||||
self.assertIn(
|
||||
f"- name: {step}\n"
|
||||
" if: steps.context.outputs.should_run == 'true' && "
|
||||
"steps.context.outputs.draft == 'true'",
|
||||
validation_workflow,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"Release was published; reverting to draft before deleting assets",
|
||||
validation_workflow,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"A published release edit introduced invalid assets",
|
||||
validation_workflow,
|
||||
)
|
||||
self.assertIn(
|
||||
|
||||
Reference in New Issue
Block a user