mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Keep workflow outputs out of generated shell
Change-source: pulse-maintainer
This commit is contained in:
@@ -36,9 +36,11 @@ six-hour default.
|
||||
Each workflow declares its default `GITHUB_TOKEN` permissions explicitly, and
|
||||
both workflow defaults and job-level overrides enumerate scopes instead of
|
||||
using `read-all`, `write-all`, or dynamic grants. Workflow inputs, secrets,
|
||||
`github.token`, and attacker-controlled GitHub event metadata are passed to
|
||||
`run` steps through `env`; they are data and must never be interpolated into
|
||||
the generated shell program.
|
||||
`github.token`, step and job outputs, and attacker-controlled GitHub event
|
||||
metadata are passed to `run` steps through `env`; they are data and must never
|
||||
be interpolated into the generated shell program. Outputs remain data even
|
||||
when an intermediate step parsed or validated them, because later substitution
|
||||
would turn their value back into shell source.
|
||||
|
||||
Workflows triggered by `pull_request` cannot reference confidential repository
|
||||
secrets. Canonical governance therefore keeps its pull-request checks local to
|
||||
|
||||
@@ -71,9 +71,11 @@ jobs:
|
||||
|
||||
- name: Run canonical completion guard against changed commits
|
||||
shell: bash
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.diff.outputs.range }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
range="${{ steps.diff.outputs.range }}"
|
||||
range="${WORKFLOW_OUTPUT_1}"
|
||||
if [ -z "${range}" ]; then
|
||||
printf '' | python3 scripts/release_control/canonical_completion_guard.py --files-from-stdin
|
||||
exit 0
|
||||
|
||||
@@ -146,17 +146,23 @@ jobs:
|
||||
|
||||
- name: Resolve required release branch
|
||||
id: branch_policy
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.extract.outputs.version }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.extract.outputs.source_branch }}
|
||||
run: |
|
||||
REQUIRED_BRANCH="$(python3 scripts/release_control/control_plane.py --branch-for-version "${{ steps.extract.outputs.version }}")"
|
||||
if [ "${{ steps.extract.outputs.source_branch }}" != "$REQUIRED_BRANCH" ]; then
|
||||
echo "::error::Invalid release line. Version ${{ steps.extract.outputs.version }} must run from ${REQUIRED_BRANCH}, but workflow ref is ${{ steps.extract.outputs.source_branch }}."
|
||||
REQUIRED_BRANCH="$(python3 scripts/release_control/control_plane.py --branch-for-version "${WORKFLOW_OUTPUT_1}")"
|
||||
if [ "${WORKFLOW_OUTPUT_2}" != "$REQUIRED_BRANCH" ]; then
|
||||
echo "::error::Invalid release line. Version ${WORKFLOW_OUTPUT_1} must run from ${REQUIRED_BRANCH}, but workflow ref is ${WORKFLOW_OUTPUT_2}."
|
||||
exit 1
|
||||
fi
|
||||
echo "required_branch=${REQUIRED_BRANCH}" >> "$GITHUB_OUTPUT"
|
||||
echo "[OK] Governed release branch for ${{ steps.extract.outputs.version }} is ${REQUIRED_BRANCH}"
|
||||
echo "[OK] Governed release branch for ${WORKFLOW_OUTPUT_1} is ${REQUIRED_BRANCH}"
|
||||
|
||||
- name: Validate release-note visual plan
|
||||
id: visual_plan
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.extract.outputs.historical_asset_backfill_only }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.extract.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PLAN_FILE=$(mktemp)
|
||||
@@ -171,9 +177,9 @@ jobs:
|
||||
count --plan "$PLAN_FILE")
|
||||
COMPARISON_TAG=""
|
||||
if [ "$CAPTURE_COUNT" -gt 0 ] && \
|
||||
[ "${{ steps.extract.outputs.historical_asset_backfill_only }}" != "true" ]; then
|
||||
[ "${WORKFLOW_OUTPUT_1}" != "true" ]; then
|
||||
COMPARISON_TAG=$(./scripts/generate-release-notes.sh \
|
||||
--resolve-base "${{ steps.extract.outputs.version }}")
|
||||
--resolve-base "${WORKFLOW_OUTPUT_2}")
|
||||
fi
|
||||
echo "capture_count=${CAPTURE_COUNT}" >> "$GITHUB_OUTPUT"
|
||||
echo "comparison_tag=${COMPARISON_TAG}" >> "$GITHUB_OUTPUT"
|
||||
@@ -181,9 +187,11 @@ jobs:
|
||||
|
||||
- name: Validate VERSION file
|
||||
if: ${{ steps.extract.outputs.historical_asset_backfill_only != 'true' }}
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.extract.outputs.version }}
|
||||
run: |
|
||||
FILE_VERSION=$(cat VERSION | tr -d '\n')
|
||||
REQUESTED_VERSION="${{ steps.extract.outputs.version }}"
|
||||
REQUESTED_VERSION="${WORKFLOW_OUTPUT_1}"
|
||||
if [ "$FILE_VERSION" != "$REQUESTED_VERSION" ]; then
|
||||
echo "::error::VERSION file ($FILE_VERSION) does not match requested version ($REQUESTED_VERSION)."
|
||||
echo "The VERSION file must be updated and committed before running release."
|
||||
@@ -196,10 +204,11 @@ jobs:
|
||||
env:
|
||||
MOBILE_RELEASE_DECISION: ${{ github.event.inputs.mobile_release_decision }}
|
||||
MOBILE_RELEASE_EVIDENCE: ${{ github.event.inputs.mobile_release_evidence }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.extract.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 scripts/release_control/mobile_release_gate.py \
|
||||
--version "${{ steps.extract.outputs.version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_1}" \
|
||||
--decision "${MOBILE_RELEASE_DECISION}" \
|
||||
--evidence "${MOBILE_RELEASE_EVIDENCE}" \
|
||||
--github-annotations
|
||||
@@ -778,13 +787,15 @@ jobs:
|
||||
|
||||
- name: Capture comparison and candidate views
|
||||
if: ${{ needs.prepare.outputs.visual_capture_count != '0' }}
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.visual_comparison_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PLAN_FILE=$(mktemp)
|
||||
jq -er '.inputs.release_screenshot_plan' "$GITHUB_EVENT_PATH" > "$PLAN_FILE"
|
||||
bash scripts/capture-release-note-visuals.sh \
|
||||
"$PLAN_FILE" \
|
||||
"${{ needs.prepare.outputs.visual_comparison_tag }}" \
|
||||
"${WORKFLOW_OUTPUT_1}" \
|
||||
release-note-visuals
|
||||
|
||||
- name: Upload release-note visual artifact
|
||||
@@ -840,18 +851,34 @@ jobs:
|
||||
path: release-note-visuals
|
||||
|
||||
- name: Verify immutable release candidate
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
python3 scripts/release_candidate_manifest.py verify-local \
|
||||
--release-dir release \
|
||||
--manifest release-candidate-manifest/release-candidate.json \
|
||||
--version "${{ needs.prepare.outputs.version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_1}" \
|
||||
--source-sha "${GITHUB_SHA}"
|
||||
|
||||
- name: Prepare release notes
|
||||
id: generate_notes
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.version }}
|
||||
WORKFLOW_OUTPUT_2: ${{ needs.prepare.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_3: ${{ needs.prepare.outputs.release_stage }}
|
||||
WORKFLOW_OUTPUT_4: ${{ needs.prepare.outputs.promoted_from_tag }}
|
||||
WORKFLOW_OUTPUT_5: ${{ needs.prepare.outputs.rollback_tag }}
|
||||
WORKFLOW_OUTPUT_6: ${{ needs.prepare.outputs.rollback_command }}
|
||||
WORKFLOW_OUTPUT_7: ${{ needs.prepare.outputs.ga_date }}
|
||||
WORKFLOW_OUTPUT_8: ${{ needs.prepare.outputs.v5_eos_date }}
|
||||
WORKFLOW_OUTPUT_9: ${{ needs.prepare.outputs.hotfix_exception }}
|
||||
WORKFLOW_OUTPUT_10: ${{ needs.prepare.outputs.hotfix_reason }}
|
||||
WORKFLOW_OUTPUT_11: ${{ needs.prepare.outputs.require_windows_signing }}
|
||||
WORKFLOW_OUTPUT_12: ${{ needs.prepare.outputs.unsigned_windows_exception }}
|
||||
WORKFLOW_OUTPUT_13: ${{ needs.prepare.outputs.unsigned_windows_reason }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
VERSION="${WORKFLOW_OUTPUT_1}"
|
||||
NOTES_FILE=$(mktemp)
|
||||
if ! jq -er '.inputs.release_notes | select(type == "string" and length > 0)' \
|
||||
"$GITHUB_EVENT_PATH" > "$NOTES_FILE"; then
|
||||
@@ -870,25 +897,25 @@ jobs:
|
||||
python3 scripts/release_control/release_note_visuals.py render \
|
||||
--plan "$VISUAL_PLAN_FILE" \
|
||||
--repository "${{ github.repository }}" \
|
||||
--tag "${{ needs.prepare.outputs.tag }}" \
|
||||
--tag "${WORKFLOW_OUTPUT_2}" \
|
||||
--output "$VISUAL_MARKDOWN_FILE"
|
||||
python3 scripts/release_control/render_release_body.py \
|
||||
--version "$VERSION" \
|
||||
--release-notes-file "$NOTES_FILE" \
|
||||
--release-visuals-file "$VISUAL_MARKDOWN_FILE" \
|
||||
--output "$RENDERED_NOTES_FILE" \
|
||||
--promotion-channel "${{ needs.prepare.outputs.release_stage }}" \
|
||||
--candidate-tag "${{ needs.prepare.outputs.tag }}" \
|
||||
--promoted-prerelease-tag "${{ needs.prepare.outputs.promoted_from_tag }}" \
|
||||
--rollback-target "${{ needs.prepare.outputs.rollback_tag }}" \
|
||||
--rollback-command "${{ needs.prepare.outputs.rollback_command }}" \
|
||||
--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 }}" \
|
||||
--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 }}"
|
||||
--promotion-channel "${WORKFLOW_OUTPUT_3}" \
|
||||
--candidate-tag "${WORKFLOW_OUTPUT_2}" \
|
||||
--promoted-prerelease-tag "${WORKFLOW_OUTPUT_4}" \
|
||||
--rollback-target "${WORKFLOW_OUTPUT_5}" \
|
||||
--rollback-command "${WORKFLOW_OUTPUT_6}" \
|
||||
--planned-ga-date "${WORKFLOW_OUTPUT_7}" \
|
||||
--planned-v5-eos-date "${WORKFLOW_OUTPUT_8}" \
|
||||
--hotfix-exception "${WORKFLOW_OUTPUT_9}" \
|
||||
--hotfix-reason "${WORKFLOW_OUTPUT_10}" \
|
||||
--require-windows-signing "${WORKFLOW_OUTPUT_11}" \
|
||||
--unsigned-windows-exception "${WORKFLOW_OUTPUT_12}" \
|
||||
--unsigned-windows-reason "${WORKFLOW_OUTPUT_13}"
|
||||
|
||||
# Customer-facing improvements provide the compact pre-update preview.
|
||||
# Historical Highlights sections remain supported for older packets.
|
||||
@@ -910,8 +937,9 @@ jobs:
|
||||
id: existing_release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
run: |
|
||||
TAG="${{ needs.prepare.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
EXISTING_RELEASE=$(gh api "repos/${{ github.repository }}/releases?per_page=100" --paginate | jq -sc --arg tag "$TAG" 'add | map(select(.tag_name == $tag)) | first // empty')
|
||||
RELEASE_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty')
|
||||
RELEASE_URL=$(echo "$EXISTING_RELEASE" | jq -r '.html_url // empty')
|
||||
@@ -928,13 +956,18 @@ jobs:
|
||||
- name: Create tag
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.existing_release.outputs.release_id }}
|
||||
WORKFLOW_OUTPUT_3: ${{ steps.existing_release.outputs.release_is_draft }}
|
||||
WORKFLOW_OUTPUT_4: ${{ steps.existing_release.outputs.release_published_at }}
|
||||
WORKFLOW_OUTPUT_5: ${{ steps.existing_release.outputs.release_activation_committed }}
|
||||
run: |
|
||||
TAG="${{ needs.prepare.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
EXISTING_RELEASE_ID="${{ steps.existing_release.outputs.release_id }}"
|
||||
EXISTING_RELEASE_DRAFT="${{ steps.existing_release.outputs.release_is_draft }}"
|
||||
EXISTING_RELEASE_PUBLISHED_AT="${{ steps.existing_release.outputs.release_published_at }}"
|
||||
EXISTING_RELEASE_ACTIVATION_COMMITTED="${{ steps.existing_release.outputs.release_activation_committed }}"
|
||||
EXISTING_RELEASE_ID="${WORKFLOW_OUTPUT_2}"
|
||||
EXISTING_RELEASE_DRAFT="${WORKFLOW_OUTPUT_3}"
|
||||
EXISTING_RELEASE_PUBLISHED_AT="${WORKFLOW_OUTPUT_4}"
|
||||
EXISTING_RELEASE_ACTIVATION_COMMITTED="${WORKFLOW_OUTPUT_5}"
|
||||
|
||||
REMOTE_TAG_SHA=$(git ls-remote --tags origin "refs/tags/${TAG}" | awk '{print $1}')
|
||||
|
||||
@@ -969,18 +1002,27 @@ jobs:
|
||||
id: create_release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.generate_notes.outputs.notes_file }}
|
||||
WORKFLOW_OUTPUT_3: ${{ needs.prepare.outputs.is_prerelease }}
|
||||
WORKFLOW_OUTPUT_4: ${{ steps.existing_release.outputs.release_id }}
|
||||
WORKFLOW_OUTPUT_5: ${{ steps.existing_release.outputs.release_url }}
|
||||
WORKFLOW_OUTPUT_6: ${{ steps.existing_release.outputs.release_is_draft }}
|
||||
WORKFLOW_OUTPUT_7: ${{ steps.existing_release.outputs.release_published_at }}
|
||||
WORKFLOW_OUTPUT_8: ${{ steps.existing_release.outputs.release_activation_committed }}
|
||||
WORKFLOW_OUTPUT_9: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ needs.prepare.outputs.tag }}"
|
||||
NOTES_FILE="${{ steps.generate_notes.outputs.notes_file }}"
|
||||
IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
NOTES_FILE="${WORKFLOW_OUTPUT_2}"
|
||||
IS_PRERELEASE="${WORKFLOW_OUTPUT_3}"
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
|
||||
RELEASE_ID="${{ steps.existing_release.outputs.release_id }}"
|
||||
RELEASE_URL="${{ steps.existing_release.outputs.release_url }}"
|
||||
IS_DRAFT="${{ steps.existing_release.outputs.release_is_draft }}"
|
||||
PUBLISHED_AT="${{ steps.existing_release.outputs.release_published_at }}"
|
||||
ACTIVATION_COMMITTED="${{ steps.existing_release.outputs.release_activation_committed }}"
|
||||
RELEASE_ID="${WORKFLOW_OUTPUT_4}"
|
||||
RELEASE_URL="${WORKFLOW_OUTPUT_5}"
|
||||
IS_DRAFT="${WORKFLOW_OUTPUT_6}"
|
||||
PUBLISHED_AT="${WORKFLOW_OUTPUT_7}"
|
||||
ACTIVATION_COMMITTED="${WORKFLOW_OUTPUT_8}"
|
||||
RELEASE_PAYLOAD=$(mktemp)
|
||||
RELEASE_JSON_FILE=$(mktemp)
|
||||
ACTUAL_BODY_FILE=$(mktemp)
|
||||
@@ -1041,7 +1083,7 @@ jobs:
|
||||
fi
|
||||
|
||||
python3 scripts/release_control/render_release_body.py \
|
||||
--version "${{ needs.prepare.outputs.version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_9}" \
|
||||
--validate-body-file "$ACTUAL_BODY_FILE" \
|
||||
--expected-body-file "$NOTES_FILE"
|
||||
|
||||
@@ -1054,8 +1096,9 @@ jobs:
|
||||
- name: Upload checksums
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
run: |
|
||||
TAG="${{ needs.prepare.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
release_upload_with_retry() {
|
||||
local attempt=1
|
||||
local max_attempts=5
|
||||
@@ -1096,9 +1139,10 @@ jobs:
|
||||
if: ${{ needs.prepare.outputs.visual_capture_count != '0' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ needs.prepare.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
PLAN_FILE=$(mktemp)
|
||||
jq -er '.inputs.release_screenshot_plan' "$GITHUB_EVENT_PATH" > "$PLAN_FILE"
|
||||
release_upload_with_retry() {
|
||||
@@ -1130,8 +1174,9 @@ jobs:
|
||||
- name: Upload release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
run: |
|
||||
TAG="${{ needs.prepare.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
release_upload_with_retry() {
|
||||
local attempt=1
|
||||
local max_attempts=5
|
||||
@@ -1236,13 +1281,18 @@ jobs:
|
||||
|
||||
- name: Stop after staging (draft only)
|
||||
if: ${{ github.event.inputs.draft_only == 'true' }}
|
||||
run: 'echo "Draft-only mode: ${{ steps.create_release.outputs.release_url }}"'
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.create_release.outputs.release_url }}
|
||||
run: 'echo "Draft-only mode: ${WORKFLOW_OUTPUT_1}"'
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.create_release.outputs.release_url }}
|
||||
run: |
|
||||
echo "[SUCCESS] Release assets staged behind an unpublished draft."
|
||||
echo "Release: ${{ needs.prepare.outputs.tag }}"
|
||||
echo "URL: ${{ steps.create_release.outputs.release_url }}"
|
||||
echo "Release: ${WORKFLOW_OUTPUT_1}"
|
||||
echo "URL: ${WORKFLOW_OUTPUT_2}"
|
||||
|
||||
backfill_release_assets:
|
||||
needs:
|
||||
@@ -1288,19 +1338,23 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
run: |
|
||||
./scripts/backfill-release-assets.sh --tag "${{ needs.prepare.outputs.tag }}" --repo "${{ github.repository }}"
|
||||
./scripts/backfill-release-assets.sh --tag "${WORKFLOW_OUTPUT_1}" --repo "${{ github.repository }}"
|
||||
|
||||
- name: Validate published release packet
|
||||
env:
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
run: |
|
||||
./scripts/validate-published-release.sh "${{ needs.prepare.outputs.tag }}" "${{ github.repository }}"
|
||||
./scripts/validate-published-release.sh "${WORKFLOW_OUTPUT_1}" "${{ github.repository }}"
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
|
||||
run: |
|
||||
echo "[SUCCESS] Historical release assets repaired"
|
||||
echo "Release: ${{ needs.prepare.outputs.tag }}"
|
||||
echo "Release: ${WORKFLOW_OUTPUT_1}"
|
||||
|
||||
publish_docker:
|
||||
needs:
|
||||
|
||||
@@ -356,5 +356,7 @@ jobs:
|
||||
echo "✓ Installed version matches ${VERSION}"
|
||||
|
||||
- name: Smoke result
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.inputs.outputs.tag }}
|
||||
run: |
|
||||
echo "::notice::install.sh smoke passed for tag ${{ steps.inputs.outputs.tag }}"
|
||||
echo "::notice::install.sh smoke passed for tag ${WORKFLOW_OUTPUT_1}"
|
||||
|
||||
@@ -216,18 +216,20 @@ jobs:
|
||||
- name: Output image information
|
||||
env:
|
||||
IMAGE_KIND: ${{ matrix.image }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.version.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "✅ Exact-version ${IMAGE_KIND} Docker image staged successfully!"
|
||||
if [[ "${IMAGE_KIND}" == "server" ]]; then
|
||||
echo "Server images (linux/amd64, linux/arm64):"
|
||||
echo " - rcourtman/pulse:${{ steps.version.outputs.tag }}"
|
||||
echo " - rcourtman/pulse:${{ steps.version.outputs.version }}"
|
||||
echo " - rcourtman/pulse:${WORKFLOW_OUTPUT_1}"
|
||||
echo " - rcourtman/pulse:${WORKFLOW_OUTPUT_2}"
|
||||
echo "Pulse Agent binaries ship as release assets, not as a Docker image."
|
||||
echo "See the GitHub release page for pulse-agent-{darwin,freebsd,linux,windows}-{amd64,arm64,...}."
|
||||
else
|
||||
echo "Control-plane images (linux/amd64, linux/arm64):"
|
||||
echo " - rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}"
|
||||
echo " - rcourtman/pulse-control-plane:${{ steps.version.outputs.version }}"
|
||||
echo " - rcourtman/pulse-control-plane:${WORKFLOW_OUTPUT_1}"
|
||||
echo " - rcourtman/pulse-control-plane:${WORKFLOW_OUTPUT_2}"
|
||||
fi
|
||||
echo "Floating aliases are promoted separately at the activation barrier."
|
||||
|
||||
|
||||
@@ -129,21 +129,26 @@ jobs:
|
||||
git checkout --detach "refs/tags/${RELEASE_TAG}"
|
||||
|
||||
- name: Align chart metadata links
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.versions.outputs.chart_version }}
|
||||
run: |
|
||||
python3 scripts/sync_chart_release_metadata.py \
|
||||
--chart deploy/helm/pulse/Chart.yaml \
|
||||
--version "${{ steps.versions.outputs.chart_version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_1}" \
|
||||
--repo "${{ github.repository }}"
|
||||
|
||||
- name: Helm lint (strict)
|
||||
run: helm lint deploy/helm/pulse --strict
|
||||
|
||||
- name: Package chart
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.versions.outputs.chart_version }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.versions.outputs.app_version }}
|
||||
run: |
|
||||
mkdir -p dist
|
||||
helm package deploy/helm/pulse \
|
||||
--version "${{ steps.versions.outputs.chart_version }}" \
|
||||
--app-version "${{ steps.versions.outputs.app_version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_1}" \
|
||||
--app-version "${WORKFLOW_OUTPUT_2}" \
|
||||
--destination dist
|
||||
|
||||
- name: Upload packaged chart artifact
|
||||
@@ -215,7 +220,9 @@ jobs:
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.versions.outputs.release_tag }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.versions.outputs.chart_version }}
|
||||
run: |
|
||||
gh release upload "${{ steps.versions.outputs.release_tag }}" \
|
||||
dist/pulse-${{ steps.versions.outputs.chart_version }}.tgz \
|
||||
gh release upload "${WORKFLOW_OUTPUT_1}" \
|
||||
"dist/pulse-${WORKFLOW_OUTPUT_2}.tgz" \
|
||||
--clobber
|
||||
|
||||
@@ -296,6 +296,7 @@ jobs:
|
||||
QUALIFIED_COMMIT: ${{ steps.source.outputs.commit }}
|
||||
RELEASE_ID: ${{ steps.release.outputs.release_id }}
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.lab.outputs.elapsed_seconds }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_dir="$RUNNER_TEMP/secure-runtime-verified"
|
||||
@@ -322,7 +323,7 @@ jobs:
|
||||
--collector-v2-signature "${release_dir}/pulse-secure-runtime-collector-v2-linux-amd64.sig" \
|
||||
--collector-v3-signature "${release_dir}/pulse-secure-runtime-collector-v3-linux-amd64.sig" \
|
||||
--collector-v4-signature "${release_dir}/pulse-agent-linux-amd64.sig" \
|
||||
--elapsed-seconds "${{ steps.lab.outputs.elapsed_seconds }}" \
|
||||
--elapsed-seconds "${WORKFLOW_OUTPUT_1}" \
|
||||
--release-candidate-tag "${TAG}" \
|
||||
--release-repository "${GITHUB_REPOSITORY}" \
|
||||
--release-id "${RELEASE_ID}" \
|
||||
@@ -335,7 +336,9 @@ jobs:
|
||||
|
||||
- name: Stop disposable systemd host
|
||||
if: ${{ always() && steps.lab.outputs.container != '' }}
|
||||
run: docker rm --force "${{ steps.lab.outputs.container }}"
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.lab.outputs.container }}
|
||||
run: docker rm --force "${WORKFLOW_OUTPUT_1}"
|
||||
|
||||
- name: Upload immutable RC qualification evidence
|
||||
if: ${{ success() }}
|
||||
|
||||
@@ -58,9 +58,11 @@ jobs:
|
||||
|
||||
- name: Resolve target-compatible recovery profile
|
||||
id: runtime_profile
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.stable.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ steps.stable.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
PROFILE_OUTPUT="$(bash .github/scripts/resolve-demo-runtime-profile.sh "refs/tags/${TAG}")"
|
||||
printf '%s\n' "$PROFILE_OUTPUT" >> "$GITHUB_OUTPUT"
|
||||
printf 'Resolved stable demo recovery profile for %s:\n%s\n' "$TAG" "$PROFILE_OUTPUT"
|
||||
|
||||
@@ -188,10 +188,11 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.qualify.outputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/verify-release-helm-chart.sh \
|
||||
"${TAG}" "${{ steps.qualify.outputs.source_sha }}" \
|
||||
"${TAG}" "${WORKFLOW_OUTPUT_1}" \
|
||||
"${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
@@ -215,10 +216,11 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.qualify.outputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/verify-release-container-images.sh \
|
||||
"${TAG}" "${{ steps.qualify.outputs.source_sha }}" \
|
||||
"${TAG}" "${WORKFLOW_OUTPUT_1}" \
|
||||
"${{ github.repository }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dispatch a fresh durable convergence owner
|
||||
|
||||
@@ -152,6 +152,7 @@ jobs:
|
||||
HOTFIX_REASON_INPUT: ${{ inputs.hotfix_reason }}
|
||||
UNSIGNED_WINDOWS_EXCEPTION_INPUT: ${{ inputs.unsigned_windows_exception }}
|
||||
UNSIGNED_WINDOWS_REASON_INPUT: ${{ inputs.unsigned_windows_reason }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.branch_policy.outputs.required_branch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -166,7 +167,7 @@ jobs:
|
||||
IS_PRERELEASE="true"
|
||||
fi
|
||||
|
||||
REQUIRED_BRANCH="${{ steps.branch_policy.outputs.required_branch }}"
|
||||
REQUIRED_BRANCH="${WORKFLOW_OUTPUT_1}"
|
||||
|
||||
if [ "${GITHUB_REF_NAME}" != "$REQUIRED_BRANCH" ]; then
|
||||
echo "::error::Rehearsal version ${VERSION} requires branch ${REQUIRED_BRANCH}, but workflow ran on ${GITHUB_REF_NAME}."
|
||||
@@ -226,6 +227,7 @@ jobs:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
MOBILE_RELEASE_DECISION: ${{ inputs.mobile_release_decision }}
|
||||
MOBILE_RELEASE_EVIDENCE: ${{ inputs.mobile_release_evidence }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.rehearsal.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DECISION="${MOBILE_RELEASE_DECISION:-}"
|
||||
@@ -235,7 +237,7 @@ jobs:
|
||||
EVIDENCE="Scheduled release dry-run watchdog; no mobile release packet is being dispatched."
|
||||
fi
|
||||
python3 scripts/release_control/mobile_release_gate.py \
|
||||
--version "${{ steps.rehearsal.outputs.version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_1}" \
|
||||
--decision "${DECISION}" \
|
||||
--evidence "${EVIDENCE}" \
|
||||
--github-annotations
|
||||
@@ -372,6 +374,20 @@ jobs:
|
||||
NOTE: ${{ inputs.note }}
|
||||
REHEARSAL_CONCLUSION: ${{ steps.rehearsal.conclusion }}
|
||||
JOB_CONCLUSION: ${{ job.status }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.rehearsal.outputs.version }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.rehearsal.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_3: ${{ steps.rehearsal.outputs.is_prerelease == 'true' && 'rc' || 'stable' }}
|
||||
WORKFLOW_OUTPUT_4: ${{ steps.rehearsal.outputs.promoted_from_tag }}
|
||||
WORKFLOW_OUTPUT_5: ${{ steps.rehearsal.outputs.rollback_tag }}
|
||||
WORKFLOW_OUTPUT_6: ${{ steps.rehearsal.outputs.rollback_command }}
|
||||
WORKFLOW_OUTPUT_7: ${{ steps.rehearsal.outputs.soak_hours }}
|
||||
WORKFLOW_OUTPUT_8: ${{ steps.rehearsal.outputs.ga_date }}
|
||||
WORKFLOW_OUTPUT_9: ${{ steps.rehearsal.outputs.v5_eos_date }}
|
||||
WORKFLOW_OUTPUT_10: ${{ steps.rehearsal.outputs.hotfix_exception }}
|
||||
WORKFLOW_OUTPUT_11: ${{ steps.rehearsal.outputs.hotfix_reason }}
|
||||
WORKFLOW_OUTPUT_12: ${{ steps.rehearsal.outputs.require_windows_signing }}
|
||||
WORKFLOW_OUTPUT_13: ${{ steps.rehearsal.outputs.unsigned_windows_exception }}
|
||||
WORKFLOW_OUTPUT_14: ${{ steps.rehearsal.outputs.unsigned_windows_reason }}
|
||||
run: |
|
||||
mkdir -p release-dry-run
|
||||
SUMMARY_FILE="release-dry-run/rc-to-ga-rehearsal-summary.md"
|
||||
@@ -399,35 +415,35 @@ jobs:
|
||||
echo ""
|
||||
echo "- Workflow run: ${RUN_URL}"
|
||||
echo "- Branch: ${GITHUB_REF_NAME}"
|
||||
echo "- Version: ${{ steps.rehearsal.outputs.version }}"
|
||||
echo "- Candidate stable tag: ${{ steps.rehearsal.outputs.tag }}"
|
||||
echo "- Promotion channel: ${{ steps.rehearsal.outputs.is_prerelease == 'true' && 'rc' || 'stable' }}"
|
||||
if [ -n "${{ steps.rehearsal.outputs.promoted_from_tag }}" ]; then
|
||||
echo "- Promoted prerelease tag: ${{ steps.rehearsal.outputs.promoted_from_tag }}"
|
||||
echo "- Version: ${WORKFLOW_OUTPUT_1}"
|
||||
echo "- Candidate stable tag: ${WORKFLOW_OUTPUT_2}"
|
||||
echo "- Promotion channel: ${WORKFLOW_OUTPUT_3}"
|
||||
if [ -n "${WORKFLOW_OUTPUT_4}" ]; then
|
||||
echo "- Promoted prerelease tag: ${WORKFLOW_OUTPUT_4}"
|
||||
fi
|
||||
if [ -n "${{ steps.rehearsal.outputs.rollback_tag }}" ]; then
|
||||
echo "- Rollback target: ${{ steps.rehearsal.outputs.rollback_tag }}"
|
||||
if [ -n "${WORKFLOW_OUTPUT_5}" ]; then
|
||||
echo "- Rollback target: ${WORKFLOW_OUTPUT_5}"
|
||||
fi
|
||||
if [ -n "${{ steps.rehearsal.outputs.rollback_command }}" ]; then
|
||||
echo "- Rollback command: \`${{ steps.rehearsal.outputs.rollback_command }}\`"
|
||||
if [ -n "${WORKFLOW_OUTPUT_6}" ]; then
|
||||
echo "- Rollback command: \`${WORKFLOW_OUTPUT_6}\`"
|
||||
fi
|
||||
if [ -n "${{ steps.rehearsal.outputs.soak_hours }}" ]; then
|
||||
echo "- Prerelease soak hours at rehearsal time: ${{ steps.rehearsal.outputs.soak_hours }}"
|
||||
if [ -n "${WORKFLOW_OUTPUT_7}" ]; then
|
||||
echo "- Prerelease soak hours at rehearsal time: ${WORKFLOW_OUTPUT_7}"
|
||||
fi
|
||||
if [ -n "${{ steps.rehearsal.outputs.ga_date }}" ]; then
|
||||
echo "- Planned GA date: ${{ steps.rehearsal.outputs.ga_date }}"
|
||||
if [ -n "${WORKFLOW_OUTPUT_8}" ]; then
|
||||
echo "- Planned GA date: ${WORKFLOW_OUTPUT_8}"
|
||||
fi
|
||||
if [ -n "${{ steps.rehearsal.outputs.v5_eos_date }}" ]; then
|
||||
echo "- Planned v5 end-of-support date: ${{ steps.rehearsal.outputs.v5_eos_date }}"
|
||||
if [ -n "${WORKFLOW_OUTPUT_9}" ]; then
|
||||
echo "- Planned v5 end-of-support date: ${WORKFLOW_OUTPUT_9}"
|
||||
fi
|
||||
echo "- Hotfix exception: ${{ steps.rehearsal.outputs.hotfix_exception }}"
|
||||
if [ -n "${{ steps.rehearsal.outputs.hotfix_reason }}" ]; then
|
||||
echo "- Hotfix reason: ${{ steps.rehearsal.outputs.hotfix_reason }}"
|
||||
echo "- Hotfix exception: ${WORKFLOW_OUTPUT_10}"
|
||||
if [ -n "${WORKFLOW_OUTPUT_11}" ]; then
|
||||
echo "- Hotfix reason: ${WORKFLOW_OUTPUT_11}"
|
||||
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 }}"
|
||||
echo "- Windows Authenticode required: ${WORKFLOW_OUTPUT_12}"
|
||||
echo "- Unsigned Windows exception: ${WORKFLOW_OUTPUT_13}"
|
||||
if [ -n "${WORKFLOW_OUTPUT_14}" ]; then
|
||||
echo "- Unsigned Windows reason: ${WORKFLOW_OUTPUT_14}"
|
||||
fi
|
||||
if [ -n "${NOTE}" ]; then
|
||||
echo "- Operator note: ${NOTE}"
|
||||
|
||||
@@ -145,9 +145,11 @@ jobs:
|
||||
fetch-tags: true
|
||||
|
||||
- name: Validate governed release line for selected tag
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.target.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ steps.target.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
VERSION="${TAG#v}"
|
||||
|
||||
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
|
||||
@@ -171,9 +173,11 @@ jobs:
|
||||
|
||||
- name: Resolve target-compatible demo runtime profile
|
||||
id: runtime_profile
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.target.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ steps.target.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
TAG_REF="refs/tags/${TAG}"
|
||||
PROFILE_OUTPUT="$(bash .github/scripts/resolve-demo-runtime-profile.sh "${TAG_REF}")"
|
||||
printf '%s\n' "$PROFILE_OUTPUT" >> "$GITHUB_OUTPUT"
|
||||
@@ -197,10 +201,13 @@ jobs:
|
||||
- name: Check release type
|
||||
env:
|
||||
VERIFY_ONLY: ${{ inputs.verify_only }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_2: ${{ needs.resolve.outputs.target }}
|
||||
WORKFLOW_OUTPUT_3: ${{ needs.resolve.outputs.environment_name }}
|
||||
run: |
|
||||
echo "Tag: ${{ needs.resolve.outputs.tag }}"
|
||||
echo "Target: ${{ needs.resolve.outputs.target }}"
|
||||
echo "Environment: ${{ needs.resolve.outputs.environment_name }}"
|
||||
echo "Tag: ${WORKFLOW_OUTPUT_1}"
|
||||
echo "Target: ${WORKFLOW_OUTPUT_2}"
|
||||
echo "Environment: ${WORKFLOW_OUTPUT_3}"
|
||||
echo "Verification only: ${VERIFY_ONLY}"
|
||||
|
||||
- name: Validate demo environment configuration
|
||||
@@ -298,9 +305,11 @@ jobs:
|
||||
|
||||
- name: Wait for release assets
|
||||
if: inputs.verify_only != true
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ needs.resolve.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
echo "Waiting for activated release assets to be available..."
|
||||
|
||||
MAX_ATTEMPTS=30
|
||||
@@ -335,9 +344,10 @@ jobs:
|
||||
if: inputs.verify_only != true
|
||||
env:
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ needs.resolve.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
TRUSTED_SSH_PUBLIC_KEY="$(
|
||||
go run ./scripts/release_update_key.go public-key-ssh \
|
||||
--public-key "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" \
|
||||
@@ -396,9 +406,11 @@ jobs:
|
||||
|
||||
- name: Resolve demo service identity
|
||||
id: config
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.target }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARGET="${{ needs.resolve.outputs.target }}"
|
||||
TARGET="${WORKFLOW_OUTPUT_1}"
|
||||
SERVICE_NAME="${DEMO_SERVICE_NAME:-}"
|
||||
if [ -z "$SERVICE_NAME" ]; then
|
||||
SERVICE_NAME="pulse"
|
||||
@@ -414,9 +426,10 @@ jobs:
|
||||
env:
|
||||
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
||||
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARGET="${{ needs.resolve.outputs.tag }}"
|
||||
TARGET="${WORKFLOW_OUTPUT_1}"
|
||||
TARGET_STRIPPED="${TARGET#v}"
|
||||
CURRENT=$(ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \
|
||||
"curl -fsS ${DEMO_LOCAL_BASE_URL}/api/version | jq -r .version")
|
||||
@@ -439,9 +452,10 @@ jobs:
|
||||
env:
|
||||
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
||||
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.config.outputs.service_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SERVICE_NAME="${{ steps.config.outputs.service_name }}"
|
||||
SERVICE_NAME="${WORKFLOW_OUTPUT_1}"
|
||||
REMOTE_SCRIPT=$(cat <<'EOF'
|
||||
set -euo pipefail
|
||||
SERVICE_NAME="$1"
|
||||
@@ -542,10 +556,12 @@ jobs:
|
||||
env:
|
||||
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
||||
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.config.outputs.service_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ needs.resolve.outputs.tag }}"
|
||||
SERVICE_NAME="${{ steps.config.outputs.service_name }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
SERVICE_NAME="${WORKFLOW_OUTPUT_2}"
|
||||
REMOTE_SCRIPT=$(cat <<'EOF'
|
||||
set -euo pipefail
|
||||
TAG="$1"
|
||||
@@ -566,22 +582,36 @@ jobs:
|
||||
env:
|
||||
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
||||
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.config.outputs.service_name }}
|
||||
WORKFLOW_OUTPUT_2: ${{ needs.resolve.outputs.mock_nodes }}
|
||||
WORKFLOW_OUTPUT_3: ${{ needs.resolve.outputs.mock_vms_per_node }}
|
||||
WORKFLOW_OUTPUT_4: ${{ needs.resolve.outputs.mock_lxcs_per_node }}
|
||||
WORKFLOW_OUTPUT_5: ${{ needs.resolve.outputs.mock_docker_hosts }}
|
||||
WORKFLOW_OUTPUT_6: ${{ needs.resolve.outputs.mock_docker_containers }}
|
||||
WORKFLOW_OUTPUT_7: ${{ needs.resolve.outputs.mock_generic_hosts }}
|
||||
WORKFLOW_OUTPUT_8: ${{ needs.resolve.outputs.mock_k8s_clusters }}
|
||||
WORKFLOW_OUTPUT_9: ${{ needs.resolve.outputs.mock_k8s_nodes }}
|
||||
WORKFLOW_OUTPUT_10: ${{ needs.resolve.outputs.mock_k8s_pods }}
|
||||
WORKFLOW_OUTPUT_11: ${{ needs.resolve.outputs.mock_k8s_deployments }}
|
||||
WORKFLOW_OUTPUT_12: ${{ needs.resolve.outputs.mock_seed_duration }}
|
||||
WORKFLOW_OUTPUT_13: ${{ needs.resolve.outputs.mock_sample_interval }}
|
||||
WORKFLOW_OUTPUT_14: ${{ needs.resolve.outputs.mock_update_interval }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SERVICE_NAME="${{ steps.config.outputs.service_name }}"
|
||||
MOCK_NODES="${{ needs.resolve.outputs.mock_nodes }}"
|
||||
MOCK_VMS_PER_NODE="${{ needs.resolve.outputs.mock_vms_per_node }}"
|
||||
MOCK_LXCS_PER_NODE="${{ needs.resolve.outputs.mock_lxcs_per_node }}"
|
||||
MOCK_DOCKER_HOSTS="${{ needs.resolve.outputs.mock_docker_hosts }}"
|
||||
MOCK_DOCKER_CONTAINERS="${{ needs.resolve.outputs.mock_docker_containers }}"
|
||||
MOCK_GENERIC_HOSTS="${{ needs.resolve.outputs.mock_generic_hosts }}"
|
||||
MOCK_K8S_CLUSTERS="${{ needs.resolve.outputs.mock_k8s_clusters }}"
|
||||
MOCK_K8S_NODES="${{ needs.resolve.outputs.mock_k8s_nodes }}"
|
||||
MOCK_K8S_PODS="${{ needs.resolve.outputs.mock_k8s_pods }}"
|
||||
MOCK_K8S_DEPLOYMENTS="${{ needs.resolve.outputs.mock_k8s_deployments }}"
|
||||
MOCK_SEED_DURATION="${{ needs.resolve.outputs.mock_seed_duration }}"
|
||||
MOCK_SAMPLE_INTERVAL="${{ needs.resolve.outputs.mock_sample_interval }}"
|
||||
MOCK_UPDATE_INTERVAL="${{ needs.resolve.outputs.mock_update_interval }}"
|
||||
SERVICE_NAME="${WORKFLOW_OUTPUT_1}"
|
||||
MOCK_NODES="${WORKFLOW_OUTPUT_2}"
|
||||
MOCK_VMS_PER_NODE="${WORKFLOW_OUTPUT_3}"
|
||||
MOCK_LXCS_PER_NODE="${WORKFLOW_OUTPUT_4}"
|
||||
MOCK_DOCKER_HOSTS="${WORKFLOW_OUTPUT_5}"
|
||||
MOCK_DOCKER_CONTAINERS="${WORKFLOW_OUTPUT_6}"
|
||||
MOCK_GENERIC_HOSTS="${WORKFLOW_OUTPUT_7}"
|
||||
MOCK_K8S_CLUSTERS="${WORKFLOW_OUTPUT_8}"
|
||||
MOCK_K8S_NODES="${WORKFLOW_OUTPUT_9}"
|
||||
MOCK_K8S_PODS="${WORKFLOW_OUTPUT_10}"
|
||||
MOCK_K8S_DEPLOYMENTS="${WORKFLOW_OUTPUT_11}"
|
||||
MOCK_SEED_DURATION="${WORKFLOW_OUTPUT_12}"
|
||||
MOCK_SAMPLE_INTERVAL="${WORKFLOW_OUTPUT_13}"
|
||||
MOCK_UPDATE_INTERVAL="${WORKFLOW_OUTPUT_14}"
|
||||
REMOTE_SCRIPT=$(cat <<'EOF'
|
||||
set -euo pipefail
|
||||
SERVICE_NAME="${1:-pulse}"
|
||||
@@ -725,6 +755,7 @@ jobs:
|
||||
env:
|
||||
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
||||
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
|
||||
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sleep 5
|
||||
@@ -734,7 +765,7 @@ jobs:
|
||||
|
||||
echo "Demo server is now running version: $VERSION"
|
||||
|
||||
TAG="${{ needs.resolve.outputs.tag }}"
|
||||
TAG="${WORKFLOW_OUTPUT_1}"
|
||||
TAG_STRIPPED="${TAG#v}"
|
||||
if [ "$VERSION" != "$TAG" ] && [ "$VERSION" != "$TAG_STRIPPED" ]; then
|
||||
echo "::error::Version mismatch! Expected $TAG but got $VERSION"
|
||||
|
||||
@@ -128,45 +128,50 @@ jobs:
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.release_id }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RELEASE_JSON_FILE=$(mktemp)
|
||||
RELEASE_BODY_FILE=$(mktemp)
|
||||
gh api "repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
||||
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}" \
|
||||
> "$RELEASE_JSON_FILE"
|
||||
jq -r '.body // ""' "$RELEASE_JSON_FILE" > "$RELEASE_BODY_FILE"
|
||||
python3 scripts/release_control/render_release_body.py \
|
||||
--version "${{ steps.context.outputs.version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_2}" \
|
||||
--validate-body-file "$RELEASE_BODY_FILE"
|
||||
|
||||
- name: Quarantine malformed release body
|
||||
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true' && steps.body_integrity.outcome == 'failure'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.target_commitish }}
|
||||
WORKFLOW_OUTPUT_3: ${{ steps.context.outputs.release_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PATCH_PAYLOAD=$(mktemp)
|
||||
RELEASE_JSON_FILE=$(mktemp)
|
||||
|
||||
jq -n \
|
||||
--arg tag "${{ steps.context.outputs.tag }}" \
|
||||
--arg target_commitish "${{ steps.context.outputs.target_commitish }}" \
|
||||
--arg tag "${WORKFLOW_OUTPUT_1}" \
|
||||
--arg target_commitish "${WORKFLOW_OUTPUT_2}" \
|
||||
'{draft: true, tag_name: $tag, target_commitish: $target_commitish}' \
|
||||
> "$PATCH_PAYLOAD"
|
||||
|
||||
gh api "repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
||||
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_3}" \
|
||||
-X PATCH \
|
||||
--input "$PATCH_PAYLOAD" > "$RELEASE_JSON_FILE"
|
||||
|
||||
ACTUAL_RELEASE_TAG=$(jq -r '.tag_name // empty' "$RELEASE_JSON_FILE")
|
||||
ACTUAL_TARGET_COMMITISH=$(jq -r '.target_commitish // empty' "$RELEASE_JSON_FILE")
|
||||
ACTUAL_DRAFT=$(jq -r '.draft // false' "$RELEASE_JSON_FILE")
|
||||
if [ "$ACTUAL_RELEASE_TAG" != "${{ steps.context.outputs.tag }}" ]; then
|
||||
echo "::error::Body-integrity quarantine detached release tag ${ACTUAL_RELEASE_TAG}; expected ${{ steps.context.outputs.tag }}."
|
||||
if [ "$ACTUAL_RELEASE_TAG" != "${WORKFLOW_OUTPUT_1}" ]; then
|
||||
echo "::error::Body-integrity quarantine detached release tag ${ACTUAL_RELEASE_TAG}; expected ${WORKFLOW_OUTPUT_1}."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ACTUAL_TARGET_COMMITISH" != "${{ steps.context.outputs.target_commitish }}" ]; then
|
||||
echo "::error::Body-integrity quarantine changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${{ steps.context.outputs.target_commitish }}."
|
||||
if [ "$ACTUAL_TARGET_COMMITISH" != "${WORKFLOW_OUTPUT_2}" ]; then
|
||||
echo "::error::Body-integrity quarantine changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${WORKFLOW_OUTPUT_2}."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ACTUAL_DRAFT" != "true" ]; then
|
||||
@@ -177,7 +182,7 @@ jobs:
|
||||
curl --fail-with-body --silent --show-error -X POST \
|
||||
-H "Authorization: token $GH_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ steps.context.outputs.target_commitish }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/statuses/${WORKFLOW_OUTPUT_2}" \
|
||||
-d '{
|
||||
"state": "failure",
|
||||
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
||||
@@ -196,12 +201,13 @@ jobs:
|
||||
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.release_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh api --paginate \
|
||||
"repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}/assets?per_page=100" \
|
||||
"repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}/assets?per_page=100" \
|
||||
--slurp > "$RUNNER_TEMP/release-assets.json"
|
||||
gh api "repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
||||
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}" \
|
||||
--jq '.body // ""' > "$RUNNER_TEMP/release-body.md"
|
||||
count="$(jq '[.[][]] | length' "$RUNNER_TEMP/release-assets.json")"
|
||||
if [ "$count" -eq 0 ]; then
|
||||
@@ -215,15 +221,17 @@ jobs:
|
||||
id: download
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.release_id }}
|
||||
run: |
|
||||
echo "Downloading all assets from release ${{ steps.context.outputs.tag }}..."
|
||||
echo "Downloading all assets from release ${WORKFLOW_OUTPUT_1}..."
|
||||
|
||||
mkdir -p release
|
||||
cd release
|
||||
|
||||
# Get asset info (id and name) - API works with GITHUB_TOKEN for draft releases
|
||||
# Use --paginate to handle releases with >30 assets
|
||||
ASSETS_JSON=$(gh api --paginate "repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}/assets")
|
||||
ASSETS_JSON=$(gh api --paginate "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_2}/assets")
|
||||
|
||||
if [ "$(echo "$ASSETS_JSON" | jq '. | length')" -eq 0 ]; then
|
||||
echo "::error::No assets found in release"
|
||||
@@ -264,8 +272,10 @@ jobs:
|
||||
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && inputs.candidate_manifest_artifact == ''
|
||||
id: docker
|
||||
continue-on-error: true
|
||||
env:
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.tag }}
|
||||
run: |
|
||||
IMAGE="rcourtman/pulse:${{ steps.context.outputs.tag }}"
|
||||
IMAGE="rcourtman/pulse:${WORKFLOW_OUTPUT_1}"
|
||||
echo "Attempting to pull Docker image: $IMAGE"
|
||||
echo "Docker Hub CDN propagation can take 2-5 minutes after push..."
|
||||
echo ""
|
||||
@@ -307,6 +317,10 @@ jobs:
|
||||
id: validate
|
||||
env:
|
||||
CANDIDATE_MANIFEST_ARTIFACT: ${{ inputs.candidate_manifest_artifact }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.version }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.target_commitish }}
|
||||
WORKFLOW_OUTPUT_3: ${{ steps.docker.outputs.image_available }}
|
||||
WORKFLOW_OUTPUT_4: ${{ steps.docker.outputs.image }}
|
||||
run: |
|
||||
set +e
|
||||
set -o pipefail
|
||||
@@ -318,19 +332,19 @@ jobs:
|
||||
python3 scripts/release_candidate_manifest.py verify-release \
|
||||
--manifest release-candidate-manifest/release-candidate.json \
|
||||
--assets-json "$RUNNER_TEMP/release-assets.json" \
|
||||
--version "${{ steps.context.outputs.version }}" \
|
||||
--source-sha "${{ steps.context.outputs.target_commitish }}" \
|
||||
--version "${WORKFLOW_OUTPUT_1}" \
|
||||
--source-sha "${WORKFLOW_OUTPUT_2}" \
|
||||
--release-body-file "$RUNNER_TEMP/release-body.md" 2>&1 | tee "$OUTPUT_FILE"
|
||||
elif [ "${{ steps.docker.outputs.image_available }}" = "true" ]; then
|
||||
elif [ "${WORKFLOW_OUTPUT_3}" = "true" ]; then
|
||||
echo "Running full validation (Docker + assets)..."
|
||||
scripts/validate-release.sh \
|
||||
"${{ steps.context.outputs.version }}" \
|
||||
"${{ steps.docker.outputs.image }}" \
|
||||
"${WORKFLOW_OUTPUT_1}" \
|
||||
"${WORKFLOW_OUTPUT_4}" \
|
||||
"release" 2>&1 | tee "$OUTPUT_FILE"
|
||||
else
|
||||
echo "Running assets-only validation (Docker image not available)..."
|
||||
scripts/validate-release.sh \
|
||||
"${{ steps.context.outputs.version }}" \
|
||||
"${WORKFLOW_OUTPUT_1}" \
|
||||
--skip-docker \
|
||||
"release" 2>&1 | tee "$OUTPUT_FILE"
|
||||
fi
|
||||
@@ -355,11 +369,12 @@ jobs:
|
||||
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && steps.validate.outputs.validation_passed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.target_commitish }}
|
||||
run: |
|
||||
curl --fail-with-body --silent --show-error -X POST \
|
||||
-H "Authorization: token $GH_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ steps.context.outputs.target_commitish }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/statuses/${WORKFLOW_OUTPUT_1}" \
|
||||
-d '{
|
||||
"state": "success",
|
||||
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
||||
@@ -371,11 +386,16 @@ jobs:
|
||||
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 }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.draft }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.release_id }}
|
||||
WORKFLOW_OUTPUT_3: ${{ steps.context.outputs.version }}
|
||||
WORKFLOW_OUTPUT_4: ${{ steps.context.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_5: ${{ steps.context.outputs.target_commitish }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "✅ Validation passed - updating release description"
|
||||
|
||||
INITIAL_STATE="${{ steps.context.outputs.draft }}"
|
||||
INITIAL_STATE="${WORKFLOW_OUTPUT_1}"
|
||||
if [ "$INITIAL_STATE" = "true" ]; then
|
||||
HEADER_LINE="## ✅ Release Asset Validation: PASSED"
|
||||
STATUS_LINE="**Status**: Ready for publication ✅"
|
||||
@@ -394,11 +414,11 @@ jobs:
|
||||
PATCH_RESPONSE=$(mktemp)
|
||||
PATCHED_BODY_FILE=$(mktemp)
|
||||
|
||||
gh api "repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
||||
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_2}" \
|
||||
> "$CURRENT_RELEASE_JSON"
|
||||
jq -r '.body // ""' "$CURRENT_RELEASE_JSON" > "$CURRENT_BODY_FILE"
|
||||
python3 scripts/release_control/render_release_body.py \
|
||||
--version "${{ steps.context.outputs.version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_3}" \
|
||||
--validate-body-file "$CURRENT_BODY_FILE" \
|
||||
--output "$CLEAN_BODY_FILE"
|
||||
|
||||
@@ -431,30 +451,30 @@ jobs:
|
||||
cat "$CLEAN_BODY_FILE" >> "$NEW_BODY_FILE"
|
||||
jq -n \
|
||||
--rawfile body "$NEW_BODY_FILE" \
|
||||
--arg tag "${{ steps.context.outputs.tag }}" \
|
||||
--arg target_commitish "${{ steps.context.outputs.target_commitish }}" \
|
||||
--arg tag "${WORKFLOW_OUTPUT_4}" \
|
||||
--arg target_commitish "${WORKFLOW_OUTPUT_5}" \
|
||||
'{body: $body, tag_name: $tag, target_commitish: $target_commitish}' \
|
||||
> "$PATCH_PAYLOAD"
|
||||
|
||||
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 }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_2}" \
|
||||
--data-binary "@$PATCH_PAYLOAD" > "$PATCH_RESPONSE"
|
||||
|
||||
ACTUAL_RELEASE_TAG=$(jq -r '.tag_name // empty' "$PATCH_RESPONSE")
|
||||
ACTUAL_TARGET_COMMITISH=$(jq -r '.target_commitish // empty' "$PATCH_RESPONSE")
|
||||
if [ "$ACTUAL_RELEASE_TAG" != "${{ steps.context.outputs.tag }}" ]; then
|
||||
echo "::error::Validation release body update detached release tag ${ACTUAL_RELEASE_TAG}; expected ${{ steps.context.outputs.tag }}."
|
||||
if [ "$ACTUAL_RELEASE_TAG" != "${WORKFLOW_OUTPUT_4}" ]; then
|
||||
echo "::error::Validation release body update detached release tag ${ACTUAL_RELEASE_TAG}; expected ${WORKFLOW_OUTPUT_4}."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ACTUAL_TARGET_COMMITISH" != "${{ steps.context.outputs.target_commitish }}" ]; then
|
||||
echo "::error::Validation release body update changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${{ steps.context.outputs.target_commitish }}."
|
||||
if [ "$ACTUAL_TARGET_COMMITISH" != "${WORKFLOW_OUTPUT_5}" ]; then
|
||||
echo "::error::Validation release body update changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${WORKFLOW_OUTPUT_5}."
|
||||
exit 1
|
||||
fi
|
||||
jq -r '.body // ""' "$PATCH_RESPONSE" > "$PATCHED_BODY_FILE"
|
||||
python3 scripts/release_control/render_release_body.py \
|
||||
--version "${{ steps.context.outputs.version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_3}" \
|
||||
--validate-body-file "$PATCHED_BODY_FILE" \
|
||||
--expected-body-file "$CLEAN_BODY_FILE"
|
||||
|
||||
@@ -462,11 +482,12 @@ jobs:
|
||||
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.target_commitish }}
|
||||
run: |
|
||||
curl --fail-with-body --silent --show-error -X POST \
|
||||
-H "Authorization: token $GH_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ steps.context.outputs.target_commitish }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/statuses/${WORKFLOW_OUTPUT_1}" \
|
||||
-d '{
|
||||
"state": "failure",
|
||||
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
||||
@@ -478,11 +499,12 @@ jobs:
|
||||
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 }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.release_id }}
|
||||
run: |
|
||||
echo "❌ Validation failed - deleting all release assets"
|
||||
|
||||
ASSET_IDS=$(curl -s -H "Authorization: token $GH_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}/assets" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}/assets" \
|
||||
| jq -r '.[].id')
|
||||
|
||||
if [ -n "$ASSET_IDS" ]; then
|
||||
@@ -503,6 +525,11 @@ jobs:
|
||||
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 }}
|
||||
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.release_id }}
|
||||
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.version }}
|
||||
WORKFLOW_OUTPUT_3: ${{ steps.validate.outputs.VALIDATION_OUTPUT }}
|
||||
WORKFLOW_OUTPUT_4: ${{ steps.context.outputs.tag }}
|
||||
WORKFLOW_OUTPUT_5: ${{ steps.context.outputs.target_commitish }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "❌ Validation failed - updating release description"
|
||||
@@ -519,15 +546,15 @@ jobs:
|
||||
PATCH_RESPONSE=$(mktemp)
|
||||
PATCHED_BODY_FILE=$(mktemp)
|
||||
|
||||
gh api "repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
||||
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}" \
|
||||
> "$CURRENT_RELEASE_JSON"
|
||||
jq -r '.body // ""' "$CURRENT_RELEASE_JSON" > "$CURRENT_BODY_FILE"
|
||||
python3 scripts/release_control/render_release_body.py \
|
||||
--version "${{ steps.context.outputs.version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_2}" \
|
||||
--validate-body-file "$CURRENT_BODY_FILE" \
|
||||
--output "$CLEAN_BODY_FILE"
|
||||
|
||||
VALIDATION_OUTPUT="${{ steps.validate.outputs.VALIDATION_OUTPUT }}"
|
||||
VALIDATION_OUTPUT="${WORKFLOW_OUTPUT_3}"
|
||||
if [ -n "$VALIDATION_OUTPUT" ]; then
|
||||
ERRORS=$(echo "$VALIDATION_OUTPUT" | grep -i '\[ERROR\]' | head -20 || echo "See workflow logs for details")
|
||||
else
|
||||
@@ -579,30 +606,30 @@ jobs:
|
||||
cat "$CLEAN_BODY_FILE" >> "$NEW_BODY_FILE"
|
||||
jq -n \
|
||||
--rawfile body "$NEW_BODY_FILE" \
|
||||
--arg tag "${{ steps.context.outputs.tag }}" \
|
||||
--arg target_commitish "${{ steps.context.outputs.target_commitish }}" \
|
||||
--arg tag "${WORKFLOW_OUTPUT_4}" \
|
||||
--arg target_commitish "${WORKFLOW_OUTPUT_5}" \
|
||||
'{body: $body, tag_name: $tag, target_commitish: $target_commitish}' \
|
||||
> "$PATCH_PAYLOAD"
|
||||
|
||||
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 }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}" \
|
||||
--data-binary "@$PATCH_PAYLOAD" > "$PATCH_RESPONSE"
|
||||
|
||||
ACTUAL_RELEASE_TAG=$(jq -r '.tag_name // empty' "$PATCH_RESPONSE")
|
||||
ACTUAL_TARGET_COMMITISH=$(jq -r '.target_commitish // empty' "$PATCH_RESPONSE")
|
||||
if [ "$ACTUAL_RELEASE_TAG" != "${{ steps.context.outputs.tag }}" ]; then
|
||||
echo "::error::Validation failure body update detached release tag ${ACTUAL_RELEASE_TAG}; expected ${{ steps.context.outputs.tag }}."
|
||||
if [ "$ACTUAL_RELEASE_TAG" != "${WORKFLOW_OUTPUT_4}" ]; then
|
||||
echo "::error::Validation failure body update detached release tag ${ACTUAL_RELEASE_TAG}; expected ${WORKFLOW_OUTPUT_4}."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ACTUAL_TARGET_COMMITISH" != "${{ steps.context.outputs.target_commitish }}" ]; then
|
||||
echo "::error::Validation failure body update changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${{ steps.context.outputs.target_commitish }}."
|
||||
if [ "$ACTUAL_TARGET_COMMITISH" != "${WORKFLOW_OUTPUT_5}" ]; then
|
||||
echo "::error::Validation failure body update changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${WORKFLOW_OUTPUT_5}."
|
||||
exit 1
|
||||
fi
|
||||
jq -r '.body // ""' "$PATCH_RESPONSE" > "$PATCHED_BODY_FILE"
|
||||
python3 scripts/release_control/render_release_body.py \
|
||||
--version "${{ steps.context.outputs.version }}" \
|
||||
--version "${WORKFLOW_OUTPUT_2}" \
|
||||
--validate-body-file "$PATCHED_BODY_FILE" \
|
||||
--expected-body-file "$CLEAN_BODY_FILE"
|
||||
|
||||
|
||||
@@ -24,11 +24,16 @@ USES_RE = re.compile(
|
||||
)
|
||||
RUN_RE = re.compile(rf"^(\s*)(?:-\s*)?{_yaml_key('run')}\s*:\s*(.*)$")
|
||||
EXPRESSION_RE = re.compile(r"\$\{\{(.*?)\}\}")
|
||||
# Workflow-call and dispatch inputs are data, not shell source. Secrets include
|
||||
# github.token because Actions makes that credential available independently of
|
||||
# an explicit secrets.GITHUB_TOKEN reference.
|
||||
# Workflow-call and dispatch inputs are data, not shell source. Step and job
|
||||
# outputs are data too: they can carry event or input values across an
|
||||
# otherwise-safe intermediate step. Secrets include github.token because
|
||||
# Actions makes that credential available independently of an explicit
|
||||
# secrets.GITHUB_TOKEN reference.
|
||||
SHELL_DATA_CONTEXT_RE = re.compile(
|
||||
r"(?<![\w.])(?:inputs|secrets)\b|(?<![\w.])github\.token\b"
|
||||
r"(?<![\w.])(?:inputs|secrets)\b|"
|
||||
r"(?<![\w.])github\.token\b|"
|
||||
r"(?<![\w.])(?:steps|needs)\b"
|
||||
r"(?=[^}\n]*(?:\.outputs\b|\[\s*['\"]outputs['\"]\s*\]))"
|
||||
)
|
||||
# GitHub documents these event fields as attacker-controlled strings. They may
|
||||
# be passed through env, but interpolating them into a generated shell program
|
||||
@@ -430,7 +435,8 @@ def audit_workflow(path: Path) -> list[Finding]:
|
||||
Finding(
|
||||
path,
|
||||
script_index + 1,
|
||||
"workflow inputs and secrets must enter run scripts through env",
|
||||
"workflow inputs, secrets, and step/job outputs "
|
||||
"must enter run scripts through env",
|
||||
)
|
||||
)
|
||||
elif UNTRUSTED_GITHUB_CONTEXT_RE.search(expression):
|
||||
|
||||
@@ -390,6 +390,28 @@ jobs:
|
||||
5,
|
||||
)
|
||||
|
||||
def test_rejects_step_and_job_outputs_in_generated_shell(self) -> None:
|
||||
findings = self.audit(
|
||||
"""permissions: {}
|
||||
jobs:
|
||||
unsafe:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- run: echo "${{ steps.parse.outputs.version }}"
|
||||
- run: echo "${{ needs.prepare.outputs.release_tag }}"
|
||||
- run: echo "${{ steps['parse']['outputs']['version'] }}"
|
||||
- env:
|
||||
VERSION: ${{ steps.parse.outputs.version }}
|
||||
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
|
||||
run: printf '%s %s\\n' "$VERSION" "$RELEASE_TAG"
|
||||
"""
|
||||
)
|
||||
self.assertEqual(
|
||||
sum("must enter run scripts through env" in finding for finding in findings),
|
||||
3,
|
||||
)
|
||||
|
||||
def test_rejects_untrusted_github_metadata_in_generated_shell(self) -> None:
|
||||
findings = self.audit(
|
||||
"""on: [pull_request]
|
||||
|
||||
Reference in New Issue
Block a user