|
| 1 | +name: Release Authentication Service |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_dispatch: |
| 5 | + inputs: |
| 6 | + release_tag: |
| 7 | + description: Semantic release tag, for example v0.2.4 |
| 8 | + required: true |
| 9 | + type: string |
| 10 | + |
| 11 | +permissions: |
| 12 | + contents: write |
| 13 | + id-token: write |
| 14 | + attestations: write |
| 15 | + artifact-metadata: write |
| 16 | + |
| 17 | +concurrency: |
| 18 | + group: authentication-service-release-${{ inputs.release_tag }} |
| 19 | + cancel-in-progress: false |
| 20 | + |
| 21 | +jobs: |
| 22 | + release: |
| 23 | + if: github.repository == 'PastureStack/authentication-service' && github.ref == 'refs/heads/main' |
| 24 | + runs-on: ubuntu-24.04 |
| 25 | + timeout-minutes: 45 |
| 26 | + env: |
| 27 | + RELEASE_TAG: ${{ inputs.release_tag }} |
| 28 | + SOURCE_SHA: ${{ github.sha }} |
| 29 | + GH_TOKEN: ${{ github.token }} |
| 30 | + |
| 31 | + steps: |
| 32 | + - name: Validate release request |
| 33 | + shell: bash |
| 34 | + run: | |
| 35 | + set -Eeuo pipefail |
| 36 | + if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then |
| 37 | + printf 'Release tag must match vMAJOR.MINOR.PATCH\n' >&2 |
| 38 | + exit 1 |
| 39 | + fi |
| 40 | + if git ls-remote --exit-code --tags \ |
| 41 | + "https://github.com/${GITHUB_REPOSITORY}.git" \ |
| 42 | + "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then |
| 43 | + printf 'Tag already exists: %s\n' "$RELEASE_TAG" >&2 |
| 44 | + exit 1 |
| 45 | + fi |
| 46 | + if gh release view "$RELEASE_TAG" \ |
| 47 | + --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then |
| 48 | + printf 'Release already exists: %s\n' "$RELEASE_TAG" >&2 |
| 49 | + exit 1 |
| 50 | + fi |
| 51 | +
|
| 52 | + - name: Check out the immutable source commit |
| 53 | + shell: bash |
| 54 | + run: | |
| 55 | + set -Eeuo pipefail |
| 56 | + git clone --filter=blob:none \ |
| 57 | + "https://github.com/${GITHUB_REPOSITORY}.git" source |
| 58 | + git -C source checkout --detach "$SOURCE_SHA" |
| 59 | + test "$(git -C source rev-parse HEAD)" = "$SOURCE_SHA" |
| 60 | + test -z "$(git -C source status --short)" |
| 61 | +
|
| 62 | + - name: Build, test, validate, and package twice |
| 63 | + shell: bash |
| 64 | + run: | |
| 65 | + set -Eeuo pipefail |
| 66 | + source_epoch="$(git -C source show -s --format=%ct HEAD)" |
| 67 | + image="pasturestack-authentication-service-release:${GITHUB_RUN_ID}" |
| 68 | + source_path="${GITHUB_WORKSPACE}/source" |
| 69 | +
|
| 70 | + docker build \ |
| 71 | + --build-arg DAPPER_HOST_ARCH=amd64 \ |
| 72 | + --tag "$image" \ |
| 73 | + --file source/Dockerfile.dapper \ |
| 74 | + source |
| 75 | +
|
| 76 | + run_ci() { |
| 77 | + docker run --rm \ |
| 78 | + --user "$(id -u):$(id -g)" \ |
| 79 | + --volume "${source_path}:/go/src/github.com/PastureStack/authentication-service" \ |
| 80 | + --env ARCH=amd64 \ |
| 81 | + --env "GOCACHE=/tmp/go-build-cache-${GITHUB_RUN_ID}" \ |
| 82 | + --env "XDG_CONFIG_HOME=/tmp/go-config-${GITHUB_RUN_ID}" \ |
| 83 | + --env "GIT_CONFIG_GLOBAL=/tmp/gitconfig-${GITHUB_RUN_ID}" \ |
| 84 | + --env "VERSION_OVERRIDE=${RELEASE_TAG}" \ |
| 85 | + --env "SOURCE_DATE_EPOCH=${source_epoch}" \ |
| 86 | + "$image" ci |
| 87 | + } |
| 88 | +
|
| 89 | + run_ci |
| 90 | + version="${RELEASE_TAG#v}" |
| 91 | + artifact="source/dist/artifacts/authentication-service-${version}-linux-amd64.tar.xz" |
| 92 | + test -s "$artifact" |
| 93 | + cp "$artifact" /tmp/authentication-service-first.tar.xz |
| 94 | +
|
| 95 | + rm -rf source/bin source/dist |
| 96 | + run_ci |
| 97 | + cmp /tmp/authentication-service-first.tar.xz "$artifact" |
| 98 | +
|
| 99 | + mkdir artifact-check |
| 100 | + tar -xJf "$artifact" -C artifact-check |
| 101 | + test -x artifact-check/authentication-service |
| 102 | + test "$(find artifact-check -maxdepth 1 -type f | wc -l)" -eq 1 |
| 103 | + artifact-check/authentication-service --version | |
| 104 | + grep -F "${version}" >/dev/null |
| 105 | + sha256sum "$artifact" | |
| 106 | + sed "s# source/dist/artifacts/# #" \ |
| 107 | + >"${artifact}.sha256" |
| 108 | + ( |
| 109 | + cd source/dist/artifacts |
| 110 | + sha256sum --check \ |
| 111 | + "authentication-service-${version}-linux-amd64.tar.xz.sha256" |
| 112 | + ) |
| 113 | + test -z "$(git -C source status --short --untracked-files=no)" |
| 114 | +
|
| 115 | + - name: Verify scanner and produce security evidence |
| 116 | + shell: bash |
| 117 | + run: | |
| 118 | + set -Eeuo pipefail |
| 119 | + trivy_version=0.73.0 |
| 120 | + trivy_sha256=2edd39da482bb4e9831962487b68f68e3928ec3137794757f54d00383d79547b |
| 121 | + trivy_source_sha=40c73e5d6166dcc0346a1ab4e94499d1572854e4 |
| 122 | + trivy_root="${RUNNER_TEMP}/trivy-${trivy_version}" |
| 123 | + trivy_asset="${trivy_root}/trivy_${trivy_version}_Linux-64bit.tar.gz" |
| 124 | + mkdir -p "$trivy_root" |
| 125 | +
|
| 126 | + curl --fail --show-error --silent --location \ |
| 127 | + "https://github.com/aquasecurity/trivy/releases/download/v${trivy_version}/trivy_${trivy_version}_Linux-64bit.tar.gz" \ |
| 128 | + --output "$trivy_asset" |
| 129 | + printf '%s %s\n' "$trivy_sha256" "$trivy_asset" | sha256sum --check |
| 130 | + trivy_attestation_status=verified |
| 131 | + attestation_error="${trivy_root}/attestation-error.txt" |
| 132 | + if ! gh attestation verify "$trivy_asset" \ |
| 133 | + --repo aquasecurity/trivy \ |
| 134 | + --signer-workflow aquasecurity/trivy/.github/workflows/reusable-release.yaml \ |
| 135 | + --source-ref "refs/tags/v${trivy_version}" \ |
| 136 | + --source-digest "$trivy_source_sha" \ |
| 137 | + --signer-digest "$trivy_source_sha" \ |
| 138 | + 2>"$attestation_error"; then |
| 139 | + if grep -Fq 'organization has an IP allow list enabled' \ |
| 140 | + "$attestation_error"; then |
| 141 | + trivy_attestation_status=unavailable-source-ip-allow-list |
| 142 | + printf '::warning::Trivy provenance API is unavailable because the source organization rejects this hosted-runner IP. Exact asset SHA-256 verification remains mandatory.\n' |
| 143 | + else |
| 144 | + cat "$attestation_error" >&2 |
| 145 | + exit 1 |
| 146 | + fi |
| 147 | + fi |
| 148 | + tar -xzf "$trivy_asset" -C "$trivy_root" |
| 149 | + test "$("${trivy_root}/trivy" --version | awk '/^Version:/ {print $2}')" = "$trivy_version" |
| 150 | +
|
| 151 | + TRIVY_BIN="${trivy_root}/trivy" \ |
| 152 | + TRIVY_CACHE_DIR="${RUNNER_TEMP}/trivy-cache" \ |
| 153 | + RELEASE_TAG="$RELEASE_TAG" \ |
| 154 | + bash source/scripts/security-scan |
| 155 | +
|
| 156 | + security_root="source/dist/security" |
| 157 | + printf '%s\n' "$trivy_attestation_status" \ |
| 158 | + >"${security_root}/trivy-attestation-verification.txt" |
| 159 | + ( |
| 160 | + cd "$security_root" |
| 161 | + sha256sum trivy-attestation-verification.txt \ |
| 162 | + >>security-evidence.sha256 |
| 163 | + ) |
| 164 | + for report in source-security-scan.json product-security-scan.json; do |
| 165 | + jq -e '.SchemaVersion == 2 and (.Results | type == "array")' \ |
| 166 | + "${security_root}/${report}" >/dev/null |
| 167 | + done |
| 168 | + jq -e '.bomFormat == "CycloneDX" and .specVersion == "1.7" and (.components | length >= 25)' \ |
| 169 | + "${security_root}/source.cdx.json" >/dev/null |
| 170 | + jq -e '.bomFormat == "CycloneDX" and .specVersion == "1.7" and (.components | length >= 20)' \ |
| 171 | + "${security_root}/product.cdx.json" >/dev/null |
| 172 | + ( |
| 173 | + cd "$security_root" |
| 174 | + sha256sum --check security-evidence.sha256 |
| 175 | + ) |
| 176 | +
|
| 177 | + version="${RELEASE_TAG#v}" |
| 178 | + printf 'RELEASE_ARTIFACT=%s\n' \ |
| 179 | + "${GITHUB_WORKSPACE}/source/dist/artifacts/authentication-service-${version}-linux-amd64.tar.xz" \ |
| 180 | + >> "$GITHUB_ENV" |
| 181 | + printf 'PRODUCT_SBOM=%s\n' \ |
| 182 | + "${GITHUB_WORKSPACE}/${security_root}/product.cdx.json" \ |
| 183 | + >> "$GITHUB_ENV" |
| 184 | + printf 'TRIVY_ATTESTATION_STATUS=%s\n' \ |
| 185 | + "$trivy_attestation_status" \ |
| 186 | + >> "$GITHUB_ENV" |
| 187 | +
|
| 188 | + - name: Attest product provenance |
| 189 | + uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 |
| 190 | + with: |
| 191 | + subject-path: ${{ env.RELEASE_ARTIFACT }} |
| 192 | + |
| 193 | + - name: Attest product SBOM |
| 194 | + uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 |
| 195 | + with: |
| 196 | + subject-path: ${{ env.RELEASE_ARTIFACT }} |
| 197 | + sbom-path: ${{ env.PRODUCT_SBOM }} |
| 198 | + |
| 199 | + - name: Publish immutable GitHub release |
| 200 | + shell: bash |
| 201 | + run: | |
| 202 | + set -Eeuo pipefail |
| 203 | + version="${RELEASE_TAG#v}" |
| 204 | + artifact="source/dist/artifacts/authentication-service-${version}-linux-amd64.tar.xz" |
| 205 | + checksum="${artifact}.sha256" |
| 206 | + security_root="source/dist/security" |
| 207 | + artifact_sha="$(sha256sum "$artifact" | awk '{print $1}')" |
| 208 | +
|
| 209 | + { |
| 210 | + printf '# PastureStack Authentication Service %s\n\n' "$RELEASE_TAG" |
| 211 | + printf 'This release provides provider-neutral OpenID Connect authorization-code authentication, a short-lived signed identity proof for explicit account linking or reassignment, transactional provider switching, and local-administrator recovery without username or email guessing.\n\n' |
| 212 | + printf '## Immutable coordinates\n\n' |
| 213 | + printf -- '- Source commit: `%s`\n' "$SOURCE_SHA" |
| 214 | + printf -- '- Artifact SHA-256: `%s`\n\n' "$artifact_sha" |
| 215 | + printf 'Two clean builds produced byte-identical archives. The full race-enabled test suite, formatting check, and static analysis passed before publication.\n\n' |
| 216 | + printf 'The release includes source and product CycloneDX 1.7 SBOMs, complete Trivy JSON reports, Critical/High gates, OpenVEX applicability evidence, checksums, and GitHub-hosted SLSA provenance and SBOM attestations. Trivy 0.73.0 was fixed by exact asset SHA-256 and version checked before use.\n\n' |
| 217 | + if [[ "$TRIVY_ATTESTATION_STATUS" == verified ]]; then |
| 218 | + printf 'The Trivy asset provenance was also verified against its release workflow, tag, and source commit.\n\n' |
| 219 | + else |
| 220 | + printf 'The Trivy source organization rejected the hosted runner through its IP allow list, so its public provenance API could not be queried. This exact limitation is recorded in the attached verification-status evidence; every other scanner verification remains fail-closed.\n\n' |
| 221 | + fi |
| 222 | + printf 'PastureStack is an independent community effort to preserve, audit, and modernize the Rancher 1.6 ecosystem. It is not affiliated with or endorsed by Rancher Labs or SUSE.\n' |
| 223 | + } >release-notes.md |
| 224 | +
|
| 225 | + gh release create "$RELEASE_TAG" \ |
| 226 | + "$artifact" \ |
| 227 | + "$checksum" \ |
| 228 | + "${security_root}/source.cdx.json" \ |
| 229 | + "${security_root}/product.cdx.json" \ |
| 230 | + "${security_root}/source-security-scan.json" \ |
| 231 | + "${security_root}/product-security-scan.json" \ |
| 232 | + "${security_root}/source-critical-high.txt" \ |
| 233 | + "${security_root}/product-critical-high.txt" \ |
| 234 | + "${security_root}/openvex.json" \ |
| 235 | + "${security_root}/security-evidence.sha256" \ |
| 236 | + "${security_root}/trivy-version.txt" \ |
| 237 | + "${security_root}/trivy-attestation-verification.txt" \ |
| 238 | + --repo "$GITHUB_REPOSITORY" \ |
| 239 | + --target "$SOURCE_SHA" \ |
| 240 | + --title "PastureStack Authentication Service ${RELEASE_TAG}" \ |
| 241 | + --notes-file release-notes.md |
0 commit comments