Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
657 changes: 657 additions & 0 deletions .github/workflows/assurance-assessment.yml

Large diffs are not rendered by default.

170 changes: 170 additions & 0 deletions .github/workflows/assurance-prerequisites.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Stage 1 of Bomly's release assurance framework: everything that must hold
# before a version is tagged. It runs on the source tree, so a failure here is
# fixed by an ordinary pull request rather than by a broken release.
#
# Auto Version calls this workflow and only tags when it passes. Release
# preflight then looks for a successful run against the commit being released,
# so a hand-made tag cannot skip it.
#
# The checks it collects are declared in docs/assurance/catalog.json.
name: Release prerequisites

on:
workflow_dispatch:
inputs:
ref:
description: Commit, branch, or tag to check (defaults to this ref).
type: string
required: false
default: ""
workflow_call:
inputs:
ref:
description: Commit, branch, or tag to check.
type: string
required: false
default: ""
assurance_tag:
description: Release tag recorded in the check results.
type: string
required: false
default: ""

permissions:
contents: read

concurrency:
group: assurance-prerequisites-${{ inputs.ref || github.ref }}
cancel-in-progress: false

jobs:
smoke:
name: End-to-end scans
uses: ./.github/workflows/smoke.yml
with:
ref: ${{ inputs.ref || github.ref }}
assurance: true
assurance_tag: ${{ inputs.assurance_tag }}

portable:
name: Platform stability
uses: ./.github/workflows/portable-assurance.yml
with:
ref: ${{ inputs.ref || github.ref }}
assurance: true
assurance_tag: ${{ inputs.assurance_tag }}

# Fuzzing is advisory: a finding is recorded in the report and does not stop a
# release. `continue-on-error` is not allowed on a job that calls a reusable
# workflow, so fuzz.yml itself skips its failing step when it is called in
# assurance mode; the verdict job below is what decides the stage.
fuzz:
name: Parser fuzzing
uses: ./.github/workflows/fuzz.yml
with:
ref: ${{ inputs.ref || github.ref }}
fuzztime: 45s
assurance: true
assurance_tag: ${{ inputs.assurance_tag }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

catalog:
name: Assurance catalog
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: go.sum

- name: Validate the assurance catalog
id: catalog
continue-on-error: true
run: make assurance-catalog

- name: Record the catalog result
if: always()
env:
BOMLY_ASSURANCE_TAG: ${{ inputs.assurance_tag }}
CATALOG_OUTCOME: ${{ steps.catalog.outcome }}
run: |
set -euo pipefail
exit_code=0
summary="The assurance catalog is valid and every fixture and expected-result file it names still matches its recorded checksum."
if [ "${CATALOG_OUTCOME}" != "success" ]; then
exit_code=1
summary="The assurance catalog is invalid or one of the files it names has changed. Open the validation step for the exact entry."
fi
checks="$(grep -c '"stage":' docs/assurance/catalog.json || true)"
# --stage and --level are explicit: when the catalog is the thing
# that is broken, the emitter cannot look them up in it.
go run ./internal/assurance/cmd emit \
--id catalog-valid --stage prerequisites --level gate \
--exit-code "${exit_code}" --summary "${summary}" \
--metric checks="${checks}" --out assurance-results --step-summary

- name: Upload check result
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: assurance-catalog
path: assurance-results
if-no-files-found: error
retention-days: 7

- name: Fail the job when the catalog is invalid
if: steps.catalog.outcome == 'failure'
run: exit 1

verdict:
name: Prerequisites verdict
if: always()
needs: [smoke, portable, fuzz, catalog]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: go.sum

- name: Collect check results
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: assurance-*
merge-multiple: true
path: assurance-results

- name: Judge the prerequisites stage
env:
BOMLY_ASSURANCE_TAG: ${{ inputs.assurance_tag }}
run: |
go run ./internal/assurance/cmd verdict \
--results assurance-results --stage prerequisites \
--tag "${BOMLY_ASSURANCE_TAG}" \
--json assurance-results/stage-prerequisites.json --step-summary

- name: Upload the stage report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: assurance-stage-prerequisites
path: assurance-results
if-no-files-found: error
retention-days: 30
28 changes: 28 additions & 0 deletions .github/workflows/auto-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ on:
- patch
- minor
- major
skip_prerequisites:
description: Tag without running the release prerequisites stage (emergency use).
required: false
type: boolean
default: false

permissions:
contents: read
Expand All @@ -20,7 +25,20 @@ concurrency:
cancel-in-progress: false

jobs:
# Stage 1 of release assurance, on the commit that is about to be tagged.
# Running it before the tag exists means a stale golden file or an
# intermittent failure is fixed by a normal pull request instead of leaving a
# broken release behind.
prerequisites:
name: Release prerequisites
if: ${{ !inputs.skip_prerequisites }}
uses: ./.github/workflows/assurance-prerequisites.yml
with:
ref: ${{ github.sha }}

bump-tag-and-release:
needs: prerequisites
if: ${{ always() && (needs.prerequisites.result == 'success' || inputs.skip_prerequisites) }}
runs-on: ubuntu-latest
environment: release

Expand All @@ -41,6 +59,16 @@ jobs:
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true

- name: Require the prerequisites stage to have run on this commit
shell: bash
env:
SKIPPED: ${{ inputs.skip_prerequisites }}
run: |
set -euo pipefail
if [[ "${SKIPPED}" == "true" ]]; then
echo "::warning::Release prerequisites were skipped for this tag. The release report will show every prerequisite check as missing."
fi

- name: Require main branch
shell: bash
run: |
Expand Down
82 changes: 80 additions & 2 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ on:
workflow_dispatch:
schedule:
- cron: "0 8 * * *"
workflow_call:
inputs:
ref:
description: Commit, branch, or tag to check out.
type: string
required: false
default: ""
fuzztime:
description: Time budget per fuzz target.
type: string
required: false
default: 2m
assurance:
description: Upload a release assurance check result.
type: boolean
required: false
default: false
assurance_tag:
description: Release tag recorded in the check result.
type: string
required: false
default: ""

permissions:
contents: read
Expand All @@ -24,6 +46,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false

- name: Set up Go
Expand All @@ -34,14 +57,69 @@ jobs:
cache-dependency-path: go.sum

- name: Run native Go fuzz targets
run: make fuzz FUZZTIME=2m
id: fuzz
continue-on-error: true
env:
FUZZ_RESULTS_JSONL: ${{ runner.temp }}/fuzz-details.jsonl
FUZZTIME: ${{ inputs.fuzztime || '2m' }}
run: make fuzz FUZZTIME="${FUZZTIME}"

- name: Record the fuzz result
if: always()
env:
BOMLY_ASSURANCE_TAG: ${{ inputs.assurance_tag }}
FUZZ_OUTCOME: ${{ steps.fuzz.outcome }}
FUZZTIME: ${{ inputs.fuzztime || '2m' }}
run: |
set -euo pipefail
details="${RUNNER_TEMP}/fuzz-details.jsonl"
targets=0
failed=0
if [ -f "${details}" ]; then
targets="$(wc -l < "${details}" | tr -d ' ')"
failed="$(grep -c '"exit_code":[^0]' "${details}" || true)"
fi
exit_code=0
summary="${targets} fuzz targets ran for ${FUZZTIME} each against their seed corpus without finding a crash."
if [ "${FUZZ_OUTCOME}" != "success" ]; then
exit_code=1
summary="${failed} of ${targets} fuzz targets found a failing input. Reproducers are attached to this run."
fi
# The details file is absent when the fuzz step died before writing
# it; the check still has to report, so the flag is conditional.
details_arg=""
if [ -f "${details}" ]; then
details_arg="--details-jsonl ${details}"
fi
# shellcheck disable=SC2086 # details_arg is a flag pair or empty
go run ./internal/assurance/cmd emit \
--id fuzz --exit-code "${exit_code}" --summary "${summary}" \
--metric targets="${targets}" --metric targets_failed="${failed}" \
${details_arg} \
--out assurance-results --step-summary
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Upload check result
if: always() && inputs.assurance
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: assurance-fuzz
path: assurance-results
if-no-files-found: error
retention-days: 7

- name: Upload fuzz failures
if: failure()
if: steps.fuzz.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: go-fuzz-failures
path: |
**/testdata/fuzz/**
if-no-files-found: ignore
retention-days: 7

# In the release prerequisites stage fuzzing is advisory: the finding is
# recorded in the check result and the stage verdict decides what happens.
# On the nightly and manual runs a finding still fails the job.
- name: Fail the job when a fuzz target failed
if: steps.fuzz.outcome == 'failure' && !inputs.assurance
run: exit 1
15 changes: 12 additions & 3 deletions .github/workflows/notify-landing-yank.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
permission-contents: write

- name: Trigger docs sync on landing page (published)
if: github.event.action == 'published'
if: github.event.action == 'published' && github.event.release.draft == false
env:
GH_TOKEN: ${{ steps.landing-token.outputs.token }}
TAG: ${{ github.event.release.tag_name }}
Expand All @@ -45,8 +45,15 @@ jobs:
{"event_type":"bomly-release","client_payload":{"version":"${TAG}","publishedAt":"${PUBLISHED_AT}"}}
EOF

# A failed pre-release assurance gate leaves a draft release that a
# maintainer deletes. Deleting a draft fires the same `deleted` event as
# yanking a live release, so deletions of drafts are ignored: nothing was
# ever published for that version. `unpublished` always refers to a live
# release (it is what turns one back into a draft), so it is not guarded.
- name: Trigger docs removal on landing page (yanked)
if: github.event.action == 'deleted' || github.event.action == 'unpublished'
if: >-
github.event.action == 'unpublished' ||
(github.event.action == 'deleted' && github.event.release.draft == false)
env:
GH_TOKEN: ${{ steps.landing-token.outputs.token }}
TAG: ${{ github.event.release.tag_name }}
Expand All @@ -59,7 +66,9 @@ jobs:
EOF

winget-yank:
if: github.event.action == 'deleted' || github.event.action == 'unpublished'
if: >-
github.event.action == 'unpublished' ||
(github.event.action == 'deleted' && github.event.release.draft == false)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand Down
Loading
Loading