|
| 1 | +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json |
| 2 | + |
| 3 | +# Publish snapshot goldens to |
| 4 | +# ghcr.io/hyperlight-dev/hyperlight-snapshot-goldens. |
| 5 | +# |
| 6 | +# Runs automatically when a merge to main changes GOLDENS_VERSION (the |
| 7 | +# version string lives in |
| 8 | +# src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs). The check-published |
| 9 | +# job reads that version and checks GHCR for its `{version}-complete` |
| 10 | +# marker. If the marker is absent, the matrix walks every (hv, cpu, |
| 11 | +# config) combination, dumps the canonical snapshot, and uploads it as a |
| 12 | +# workflow artifact. A single publish job then downloads every artifact, |
| 13 | +# pushes each as a tag named `{version}-{hv}-{cpu}-{profile}`, and |
| 14 | +# pushes the marker last. Publishing the whole set from one job means a |
| 15 | +# partial run leaves no marker and is republished on the next run. |
| 16 | +# |
| 17 | +# A version whose marker exists is left untouched, so a merge that does |
| 18 | +# not bump the version, or a re-run of the same version, is a no-op. |
| 19 | +# Manual dispatch with `force: true` overwrites an existing version and |
| 20 | +# exists for recovery only. |
| 21 | +# |
| 22 | +# See docs/snapshot-versioning.md |
| 23 | + |
| 24 | +name: Regenerate Snapshot Goldens |
| 25 | + |
| 26 | +on: |
| 27 | + push: |
| 28 | + branches: [main] |
| 29 | + paths: |
| 30 | + - src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs |
| 31 | + workflow_dispatch: |
| 32 | + inputs: |
| 33 | + version: |
| 34 | + description: Goldens version string. Must match GOLDENS_VERSION in source (e.g. "v1.0"). |
| 35 | + required: true |
| 36 | + type: string |
| 37 | + force: |
| 38 | + description: Overwrite tags even if the version is already published (recovery only). |
| 39 | + type: boolean |
| 40 | + default: false |
| 41 | + |
| 42 | +env: |
| 43 | + CARGO_TERM_COLOR: always |
| 44 | + RUST_BACKTRACE: full |
| 45 | + GHCR_IMAGE: ghcr.io/hyperlight-dev/hyperlight-snapshot-goldens |
| 46 | + |
| 47 | +permissions: |
| 48 | + contents: read |
| 49 | + packages: write |
| 50 | + |
| 51 | +concurrency: |
| 52 | + group: regen-snapshot-goldens-${{ github.ref }} |
| 53 | + cancel-in-progress: false |
| 54 | + |
| 55 | +defaults: |
| 56 | + run: |
| 57 | + shell: bash |
| 58 | + |
| 59 | +jobs: |
| 60 | + check-published: |
| 61 | + runs-on: ubuntu-latest |
| 62 | + permissions: |
| 63 | + contents: read |
| 64 | + packages: read |
| 65 | + outputs: |
| 66 | + version: ${{ steps.decide.outputs.version }} |
| 67 | + needs_publish: ${{ steps.decide.outputs.needs_publish }} |
| 68 | + steps: |
| 69 | + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 |
| 70 | + |
| 71 | + - name: Install oras |
| 72 | + uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2.0.0 |
| 73 | + with: |
| 74 | + version: 1.3.2 |
| 75 | + |
| 76 | + - name: Decide version and whether to publish |
| 77 | + id: decide |
| 78 | + env: |
| 79 | + EVENT_NAME: ${{ github.event_name }} |
| 80 | + INPUT_VERSION: ${{ inputs.version }} |
| 81 | + FORCE: ${{ inputs.force }} |
| 82 | + GHCR_USER: ${{ github.actor }} |
| 83 | + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 84 | + run: | |
| 85 | + set -euo pipefail |
| 86 | + SRC=$(grep -oE 'GOLDENS_VERSION: &str = "[^"]+"' src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs | head -n1 | sed -E 's/.*"([^"]+)".*/\1/') |
| 87 | + if ! [[ "${SRC}" =~ ^v[0-9]+\.[0-9]+$ ]]; then |
| 88 | + echo "::error::GOLDENS_VERSION in source must match ^v[0-9]+\.[0-9]+$ (e.g. v1.0), found '${SRC}'" |
| 89 | + exit 1 |
| 90 | + fi |
| 91 | +
|
| 92 | + # On manual dispatch the input must name the version that the |
| 93 | + # dispatched ref actually carries. This catches a stale input. |
| 94 | + if [ "${EVENT_NAME}" = "workflow_dispatch" ] && [ "${INPUT_VERSION}" != "${SRC}" ]; then |
| 95 | + echo "::error::version input '${INPUT_VERSION}' does not match GOLDENS_VERSION in source '${SRC}'" |
| 96 | + exit 1 |
| 97 | + fi |
| 98 | +
|
| 99 | + echo "version=${SRC}" >> "$GITHUB_OUTPUT" |
| 100 | +
|
| 101 | + if [ "${EVENT_NAME}" = "workflow_dispatch" ] && [ "${FORCE}" = "true" ]; then |
| 102 | + echo "force requested: will publish ${SRC} even if it already exists" |
| 103 | + echo "needs_publish=true" >> "$GITHUB_OUTPUT" |
| 104 | + exit 0 |
| 105 | + fi |
| 106 | +
|
| 107 | + # A version is frozen once its completion marker exists on |
| 108 | + # GHCR. The marker is pushed only after every matrix job has |
| 109 | + # uploaded its tag, so a partial push (some jobs failed) |
| 110 | + # leaves no marker and the next run republishes the missing |
| 111 | + # combinations. Publishing only when the marker is absent makes the |
| 112 | + # workflow idempotent and never clobbers a complete baseline. |
| 113 | + echo "${GHCR_TOKEN}" | oras login ghcr.io -u "${GHCR_USER}" --password-stdin |
| 114 | + if oras repo tags "${GHCR_IMAGE}" 2>/dev/null | grep -qxF "${SRC}-complete"; then |
| 115 | + echo "${SRC} already published (marker ${SRC}-complete present). Nothing to do." |
| 116 | + echo "needs_publish=false" >> "$GITHUB_OUTPUT" |
| 117 | + else |
| 118 | + echo "${SRC} not fully published yet. Will publish." |
| 119 | + echo "needs_publish=true" >> "$GITHUB_OUTPUT" |
| 120 | + fi |
| 121 | +
|
| 122 | + build-guests: |
| 123 | + needs: check-published |
| 124 | + if: needs.check-published.outputs.needs_publish == 'true' |
| 125 | + strategy: |
| 126 | + matrix: |
| 127 | + config: [debug, release] |
| 128 | + uses: ./.github/workflows/dep_build_guests.yml |
| 129 | + with: |
| 130 | + config: ${{ matrix.config }} |
| 131 | + secrets: inherit |
| 132 | + |
| 133 | + generate-snapshots: |
| 134 | + needs: [check-published, build-guests] |
| 135 | + if: needs.check-published.outputs.needs_publish == 'true' |
| 136 | + strategy: |
| 137 | + fail-fast: false |
| 138 | + matrix: |
| 139 | + hypervisor: [kvm, mshv3, hyperv-ws2025] |
| 140 | + cpu: [amd, intel] |
| 141 | + config: [debug, release] |
| 142 | + runs-on: ${{ fromJson( |
| 143 | + format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}", "JobId=regen-goldens-{3}-{4}-{5}-{6}"]', |
| 144 | + matrix.hypervisor == 'hyperv-ws2025' && 'Windows' || 'Linux', |
| 145 | + matrix.hypervisor == 'hyperv-ws2025' && 'win2025' || matrix.hypervisor == 'mshv3' && 'azlinux3-mshv' || matrix.hypervisor, |
| 146 | + matrix.cpu, |
| 147 | + matrix.config, |
| 148 | + github.run_id, |
| 149 | + github.run_number, |
| 150 | + github.run_attempt)) }} |
| 151 | + steps: |
| 152 | + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 |
| 153 | + |
| 154 | + - uses: hyperlight-dev/ci-setup-workflow@f6bd9cc86d0737976d2128c8b8ced8edc017cbb4 # v1.9.0 |
| 155 | + with: |
| 156 | + rust-toolchain: "1.94" |
| 157 | + env: |
| 158 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 159 | + |
| 160 | + - name: Fix cargo home permissions |
| 161 | + if: runner.os == 'Linux' |
| 162 | + run: sudo chown -R $(id -u):$(id -g) /opt/cargo || true |
| 163 | + |
| 164 | + - name: Download Rust guests |
| 165 | + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 |
| 166 | + with: |
| 167 | + name: rust-guests-${{ matrix.config }} |
| 168 | + path: src/tests/rust_guests/bin/${{ matrix.config }}/ |
| 169 | + |
| 170 | + - name: Confirm source matches resolved version |
| 171 | + env: |
| 172 | + RESOLVED_VERSION: ${{ needs.check-published.outputs.version }} |
| 173 | + run: | |
| 174 | + set -euo pipefail |
| 175 | + SRC=$(grep -oE 'GOLDENS_VERSION: &str = "[^"]+"' src/hyperlight_host/tests/snapshot_goldens/goldens_version.rs | head -n1 | sed -E 's/.*"([^"]+)".*/\1/') |
| 176 | + if [ "${SRC}" != "${RESOLVED_VERSION}" ]; then |
| 177 | + echo "::error::source GOLDENS_VERSION '${SRC}' does not match resolved '${RESOLVED_VERSION}'" |
| 178 | + exit 1 |
| 179 | + fi |
| 180 | +
|
| 181 | + - name: Generate snapshots |
| 182 | + run: just snapshot-goldens-generate ${{ matrix.config }} |
| 183 | + |
| 184 | + - name: Resolve produced tag |
| 185 | + id: tag |
| 186 | + env: |
| 187 | + GOLDENS_VERSION: ${{ needs.check-published.outputs.version }} |
| 188 | + run: | |
| 189 | + set -euo pipefail |
| 190 | + layout=$(echo "target/snapshot-goldens/${GOLDENS_VERSION}"/*/) |
| 191 | + echo "tag=$(basename "${layout%/}")" >> "$GITHUB_OUTPUT" |
| 192 | +
|
| 193 | + - name: Upload golden layout |
| 194 | + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 |
| 195 | + with: |
| 196 | + name: golden-${{ steps.tag.outputs.tag }} |
| 197 | + path: target/snapshot-goldens/${{ needs.check-published.outputs.version }}/${{ steps.tag.outputs.tag }}/ |
| 198 | + if-no-files-found: error |
| 199 | + retention-days: 1 |
| 200 | + |
| 201 | + # Push every matrix job's snapshot from this single job, so the published set is |
| 202 | + # whole or absent. `generate-snapshots` runs `fail-fast: false` and uploads each |
| 203 | + # snapshot as an artifact, so this job's `needs` succeeds only when |
| 204 | + # all matrix jobs did. It downloads every artifact, pushes each tag, then |
| 205 | + # pushes the `{version}-complete` marker that `check-published` gates on. A |
| 206 | + # push that dies partway leaves no marker, so the next run republishes. |
| 207 | + publish: |
| 208 | + needs: [check-published, generate-snapshots] |
| 209 | + if: needs.check-published.outputs.needs_publish == 'true' |
| 210 | + runs-on: ubuntu-latest |
| 211 | + steps: |
| 212 | + - name: Install oras |
| 213 | + uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2.0.0 |
| 214 | + with: |
| 215 | + version: 1.3.2 |
| 216 | + |
| 217 | + - name: Download all golden layouts |
| 218 | + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 |
| 219 | + with: |
| 220 | + pattern: golden-* |
| 221 | + path: layouts |
| 222 | + |
| 223 | + - name: Push goldens and completion marker |
| 224 | + env: |
| 225 | + GHCR_USER: ${{ github.actor }} |
| 226 | + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 227 | + GOLDENS_VERSION: ${{ needs.check-published.outputs.version }} |
| 228 | + run: | |
| 229 | + set -euo pipefail |
| 230 | + echo "${GHCR_TOKEN}" | oras login ghcr.io -u "${GHCR_USER}" --password-stdin |
| 231 | + for layout in layouts/golden-*/; do |
| 232 | + tag=$(basename "${layout%/}") |
| 233 | + tag=${tag#golden-} |
| 234 | + echo "::group::push ${tag}" |
| 235 | + oras cp --from-oci-layout "${layout%/}:${tag}" "${GHCR_IMAGE}:${tag}" |
| 236 | + echo "::endgroup::" |
| 237 | + done |
| 238 | + printf '%s' "${GOLDENS_VERSION}" > complete.txt |
| 239 | + oras push "${GHCR_IMAGE}:${GOLDENS_VERSION}-complete" \ |
| 240 | + --artifact-type application/vnd.hyperlight.goldens.complete.v1 \ |
| 241 | + complete.txt:text/plain |
0 commit comments