ci(www): separate release and deploy into manual gated workflows
packages/kits - next/required / Setup - Stage (push) Successful in 41s
packages/ssg - next/required / Setup - Stage (push) Successful in 41s
packages/ui - next/required / Setup - Stage (push) Successful in 40s
services/docs - next/required / Setup - Hugo (push) Successful in 4s
packages/stl - next/required / Setup - Stage (push) Successful in 40s
services/static - next/required / Setup - Stage (push) Successful in 40s
services/www - next/required / Setup - Stage (push) Successful in 40s
packages/kits - next/required / Build - Configs & UI (push) Successful in 32s
packages/ssg - next/required / Build - Configs (push) Successful in 19s
packages/stl - next/required / Build - Configs (push) Successful in 19s
packages/ui - next/required / Build - Configs (push) Successful in 25s
services/docs - next/required / Build - Hugo Site (push) Successful in 4s
services/static - next/required / Build - Configs (push) Successful in 23s
services/www - next/required / Build - Configs (push) Successful in 24s
packages/kits - next/required / Lint - Kits (push) Successful in 53s
packages/kits - next/required / Test - Unit (push) Successful in 27s
packages/kits - next/required / Typecheck - Kits (push) Successful in 21s
packages/kits - next/required / Build - Storybook (push) Successful in 31s
packages/kits - next/required / Build - Kits (push) Successful in 24s
packages/ssg - next/required / Lint - SSG (push) Successful in 23s
packages/ssg - next/required / Typecheck - SSG (push) Successful in 19s
packages/ssg - next/required / Build - SSG (push) Successful in 20s
packages/stl - next/required / Lint - STL (push) Successful in 22s
packages/stl - next/required / Typecheck - STL (push) Successful in 19s
packages/stl - next/required / Test - STL (push) Successful in 22s
packages/stl - next/required / Build - STL (push) Successful in 20s
packages/ui - next/required / Lint - UI (push) Successful in 31s
packages/ui - next/required / Typecheck - UI (push) Successful in 21s
packages/ui - next/required / Test - Unit (push) Successful in 28s
packages/ui - next/required / Build - Storybook (push) Successful in 30s
packages/ui - next/required / Build - UI (push) Successful in 23s
services/docs - next/required / Release - Docker Image (push) Successful in 42s
services/static - next/required / Lint - Static (push) Successful in 24s
services/static - next/required / Typecheck - Static (push) Successful in 21s
services/static - next/required / Build - Static Site (push) Successful in 23s
services/www - next/required / Lint - WWW (push) Successful in 27s
services/www - next/required / Typecheck - WWW (push) Successful in 22s
services/www - next/required / Build - Storybook (push) Successful in 29s
services/www - next/required / Test - Unit (push) Successful in 20s
services/www - next/required / Build - WWW Service (push) Successful in 41s
packages/kits - next/required / Test - Storybook (push) Successful in 4m28s
packages/ui - next/required / Test - Storybook (push) Successful in 4m23s
services/docs - next/required / Deploy - Kubernetes (GitOps) (push) Failing after 2s
services/docs - next/required / Verify - Production Rollout (push) Skipped
services/www - next/required / Test - Storybook (push) Successful in 2m8s
services/static - next/required / Deploy - S3 (push) Failing after 21s
services/www - next/required / Build - Docker Image (push) Successful in 1m40s
packages/kits - next/required / Setup - Stage (push) Successful in 41s
packages/ssg - next/required / Setup - Stage (push) Successful in 41s
packages/ui - next/required / Setup - Stage (push) Successful in 40s
services/docs - next/required / Setup - Hugo (push) Successful in 4s
packages/stl - next/required / Setup - Stage (push) Successful in 40s
services/static - next/required / Setup - Stage (push) Successful in 40s
services/www - next/required / Setup - Stage (push) Successful in 40s
packages/kits - next/required / Build - Configs & UI (push) Successful in 32s
packages/ssg - next/required / Build - Configs (push) Successful in 19s
packages/stl - next/required / Build - Configs (push) Successful in 19s
packages/ui - next/required / Build - Configs (push) Successful in 25s
services/docs - next/required / Build - Hugo Site (push) Successful in 4s
services/static - next/required / Build - Configs (push) Successful in 23s
services/www - next/required / Build - Configs (push) Successful in 24s
packages/kits - next/required / Lint - Kits (push) Successful in 53s
packages/kits - next/required / Test - Unit (push) Successful in 27s
packages/kits - next/required / Typecheck - Kits (push) Successful in 21s
packages/kits - next/required / Build - Storybook (push) Successful in 31s
packages/kits - next/required / Build - Kits (push) Successful in 24s
packages/ssg - next/required / Lint - SSG (push) Successful in 23s
packages/ssg - next/required / Typecheck - SSG (push) Successful in 19s
packages/ssg - next/required / Build - SSG (push) Successful in 20s
packages/stl - next/required / Lint - STL (push) Successful in 22s
packages/stl - next/required / Typecheck - STL (push) Successful in 19s
packages/stl - next/required / Test - STL (push) Successful in 22s
packages/stl - next/required / Build - STL (push) Successful in 20s
packages/ui - next/required / Lint - UI (push) Successful in 31s
packages/ui - next/required / Typecheck - UI (push) Successful in 21s
packages/ui - next/required / Test - Unit (push) Successful in 28s
packages/ui - next/required / Build - Storybook (push) Successful in 30s
packages/ui - next/required / Build - UI (push) Successful in 23s
services/docs - next/required / Release - Docker Image (push) Successful in 42s
services/static - next/required / Lint - Static (push) Successful in 24s
services/static - next/required / Typecheck - Static (push) Successful in 21s
services/static - next/required / Build - Static Site (push) Successful in 23s
services/www - next/required / Lint - WWW (push) Successful in 27s
services/www - next/required / Typecheck - WWW (push) Successful in 22s
services/www - next/required / Build - Storybook (push) Successful in 29s
services/www - next/required / Test - Unit (push) Successful in 20s
services/www - next/required / Build - WWW Service (push) Successful in 41s
packages/kits - next/required / Test - Storybook (push) Successful in 4m28s
packages/ui - next/required / Test - Storybook (push) Successful in 4m23s
services/docs - next/required / Deploy - Kubernetes (GitOps) (push) Failing after 2s
services/docs - next/required / Verify - Production Rollout (push) Skipped
services/www - next/required / Test - Storybook (push) Successful in 2m8s
services/static - next/required / Deploy - S3 (push) Failing after 21s
services/www - next/required / Build - Docker Image (push) Successful in 1m40s
This commit is contained in:
1 parent
0c2474ba20
commit
5c86a13559
4 files changed
+186
-153
No files matched your search
@@ -3,74 +3,86 @@ name: services/www - manual deploy
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tested_sha:
|
||||
description: Full tested source commit
|
||||
required: true
|
||||
description: Full tested source commit (leave blank for latest)
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
tested_digest:
|
||||
description: OCI sha256 digest from the successful build
|
||||
required: true
|
||||
description: OCI sha256 digest from the successful build (leave blank for latest)
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
build_run_id:
|
||||
description: Successful WWW build run ID
|
||||
required: true
|
||||
description: Successful WWW build run ID (leave blank for latest)
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
release_tag:
|
||||
description: Previously approved release tag
|
||||
required: true
|
||||
description: Previously approved release tag (leave blank for latest)
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
release_run_id:
|
||||
description: Successful manually approved release run ID
|
||||
required: true
|
||||
description: Approved release run ID (leave blank for latest)
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
actions: read
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy - Kubernetes (GitOps)
|
||||
runs-on: apex-debian13
|
||||
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/next'
|
||||
&& github.actor == 'deytenit'
|
||||
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/next' && github.actor == 'deytenit'
|
||||
outputs:
|
||||
short_sha: ${{ steps.provenance.outputs.short_sha }}
|
||||
tested_digest: ${{ steps.provenance.outputs.tested_digest }}
|
||||
release_tag: ${{ steps.provenance.outputs.release_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: next
|
||||
- name: Authenticate local registry
|
||||
run: python3 ci/scripts/docker.py login
|
||||
env:
|
||||
REGISTRY: git.ermnvldmr.com
|
||||
REGISTRY_USER: deytenit
|
||||
REGISTRY_TOKEN: ${{ secrets.DEPLOY_REGISTRY_READ_TOKEN }}
|
||||
- name: Verify successful build and immutable digest
|
||||
REGISTRY_USER: ${{ github.repository_owner }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Verify successful build and approved release
|
||||
id: provenance
|
||||
run: python3 ci/scripts/provenance.py
|
||||
env:
|
||||
IS_DEPLOY: 'true'
|
||||
TESTED_SHA: ${{ inputs.tested_sha }}
|
||||
TESTED_DIGEST: ${{ inputs.tested_digest }}
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
PROVENANCE_TOKEN: ${{ github.token }}
|
||||
RELEASE_RUN_ID: ${{ inputs.release_run_id }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
- name: Verify approved release and deploy digest
|
||||
env:
|
||||
TESTED_SHA: ${{ inputs.tested_sha }}
|
||||
TESTED_DIGEST: ${{ inputs.tested_digest }}
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
PROVENANCE_TOKEN: ${{ github.token }}
|
||||
RELEASE_RUN_ID: ${{ inputs.release_run_id }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
GIT_WRITE_KEY: ${{ secrets.WWW_DEPLOY_KEY }}
|
||||
run: 'set -euo pipefail
|
||||
|
||||
[[ "$RELEASE_TAG" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]
|
||||
|
||||
test "$(git rev-parse "$RELEASE_TAG^{commit}")" = "$TESTED_SHA"
|
||||
|
||||
test "$(docker buildx imagetools inspect "git.ermnvldmr.com/deytenit/ermnvldmr.com/www:$RELEASE_TAG"
|
||||
--format ''{{.Manifest.Digest}}'')" = "$TESTED_DIGEST"
|
||||
- name: Setup Kustomize
|
||||
run: test "$(kustomize version)" = v5.3.0
|
||||
- name: Deploy via GitOps
|
||||
run: |
|
||||
RELEASE_TAG="${{ steps.provenance.outputs.release_tag }}"
|
||||
TESTED_DIGEST="${{ steps.provenance.outputs.tested_digest }}"
|
||||
TESTED_SHA="${{ steps.provenance.outputs.tested_sha }}"
|
||||
|
||||
echo "Deploying www digest $TESTED_DIGEST ($RELEASE_TAG)..."
|
||||
bash ci/scripts/deploy-gitops.sh www "$TESTED_DIGEST"
|
||||
|
||||
'
|
||||
- name: Await production rollout
|
||||
run: bash ci/scripts/await-rollout.sh https://www.ermnvldmr.com/version.json "sha-${TESTED_SHA:0:7}"
|
||||
env:
|
||||
TESTED_SHA: ${{ inputs.tested_sha }}
|
||||
GIT_WRITE_KEY: ${{ secrets.WWW_DEPLOY_KEY }}
|
||||
|
||||
verify-rollout:
|
||||
name: Verify - Production Rollout
|
||||
needs: deploy
|
||||
runs-on: apex-debian13
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: next
|
||||
- name: Await production rollout
|
||||
run: |
|
||||
bash ci/scripts/await-rollout.sh www "${{ needs.deploy.outputs.short_sha }}"
|
||||
@@ -318,24 +318,14 @@ jobs:
|
||||
packages: write
|
||||
outputs:
|
||||
short_sha: ${{ steps.vars.outputs.short_sha }}
|
||||
version: ${{ steps.vars.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Compute version and metadata
|
||||
- name: Calculate Short SHA
|
||||
id: vars
|
||||
run: |
|
||||
SHORT_SHA="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
VERSION=$(bash ci/scripts/calver-release.sh www)
|
||||
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
- name: Create and push release tag
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/next'
|
||||
run: bash ci/scripts/calver-release.sh tag www "${{ github.sha }}" "${{ steps.vars.outputs.version }}"
|
||||
env:
|
||||
GIT_WRITE_KEY: ${{ secrets.WWW_DEPLOY_KEY }}
|
||||
- name: Log in to Gitea Container Registry
|
||||
run: python3 ci/scripts/docker.py login
|
||||
env:
|
||||
@@ -348,8 +338,6 @@ jobs:
|
||||
env:
|
||||
IMAGES: git.ermnvldmr.com/${{ github.repository }}/www
|
||||
TAG_RULES: |
|
||||
type=raw,value=latest
|
||||
type=raw,value=${{ steps.vars.outputs.version }}
|
||||
type=sha,format=short
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
- name: Build and push Docker image
|
||||
@@ -359,7 +347,7 @@ jobs:
|
||||
LABELS: ${{ steps.meta.outputs.labels }}
|
||||
BUILD_ARGS: |
|
||||
COMMIT_SHA=${{ steps.vars.outputs.short_sha }}
|
||||
VERSION=${{ steps.vars.outputs.version }}
|
||||
VERSION=${{ steps.vars.outputs.short_sha }}
|
||||
DOCKERFILE: ci/docker/www/Dockerfile
|
||||
PLATFORMS: ''
|
||||
PUSH: 'true'
|
||||
@@ -367,41 +355,23 @@ jobs:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
id: image
|
||||
|
||||
deploy:
|
||||
name: Deploy - Kubernetes (GitOps)
|
||||
needs: build-docker
|
||||
runs-on: apex-debian13
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/next'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: next
|
||||
- name: Setup Kustomize
|
||||
run: test "$(kustomize version)" = v5.3.0
|
||||
- name: Deploy via GitOps
|
||||
run: |
|
||||
WWW_TAG="${{ needs.build-docker.outputs.version }}"
|
||||
bash ci/scripts/deploy-gitops.sh www "${WWW_TAG}"
|
||||
- name: Record tested digest provenance
|
||||
env:
|
||||
GIT_WRITE_KEY: ${{ secrets.WWW_DEPLOY_KEY }}
|
||||
|
||||
verify-rollout:
|
||||
name: Verify - Production Rollout
|
||||
needs:
|
||||
- build-docker
|
||||
- deploy
|
||||
runs-on: apex-debian13
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/next'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: next
|
||||
- name: Await production rollout
|
||||
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
|
||||
run: |
|
||||
WWW_TAG="${{ needs.build-docker.outputs.version }}"
|
||||
bash ci/scripts/await-rollout.sh www "${WWW_TAG}"
|
||||
mkdir -p .ci
|
||||
python3 - <<'PROVENANCE'
|
||||
import json, os
|
||||
from pathlib import Path
|
||||
Path('.ci/www-provenance.json').write_text(json.dumps({
|
||||
'sha': os.environ['GITHUB_SHA'],
|
||||
'digest': os.environ['IMAGE_DIGEST'],
|
||||
'run_id': os.environ['GITHUB_RUN_ID'],
|
||||
'repository': os.environ['GITHUB_REPOSITORY']
|
||||
}))
|
||||
PROVENANCE
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: www-provenance
|
||||
path: .ci/www-provenance.json
|
||||
retention-days: 30
|
||||
@@ -3,34 +3,40 @@ name: services/www - manual release
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tested_sha:
|
||||
description: Full tested source commit
|
||||
required: true
|
||||
description: Full tested source commit (leave blank for latest)
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
tested_digest:
|
||||
description: OCI sha256 digest from the successful build
|
||||
required: true
|
||||
description: OCI sha256 digest (leave blank for latest)
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
build_run_id:
|
||||
description: Successful WWW build run ID
|
||||
required: true
|
||||
description: Successful WWW build run ID (leave blank for latest)
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
packages: write
|
||||
actions: read
|
||||
jobs:
|
||||
release:
|
||||
name: Release - Tag and Promote
|
||||
runs-on: apex-debian13
|
||||
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/next'
|
||||
&& github.actor == 'deytenit'
|
||||
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/next' && github.actor == 'deytenit'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Authenticate local registry
|
||||
run: python3 ci/scripts/docker.py login
|
||||
env:
|
||||
REGISTRY: git.ermnvldmr.com
|
||||
REGISTRY_USER: deytenit
|
||||
REGISTRY_TOKEN: ${{ secrets.RELEASE_REGISTRY_TOKEN }}
|
||||
REGISTRY_USER: ${{ github.repository_owner }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Verify successful build and immutable digest
|
||||
id: provenance
|
||||
run: python3 ci/scripts/provenance.py
|
||||
@@ -41,46 +47,43 @@ jobs:
|
||||
PROVENANCE_TOKEN: ${{ github.token }}
|
||||
- name: Promote tested digest and create release tag
|
||||
env:
|
||||
TESTED_SHA: ${{ inputs.tested_sha }}
|
||||
TESTED_DIGEST: ${{ inputs.tested_digest }}
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
TESTED_SHA: ${{ steps.provenance.outputs.tested_sha }}
|
||||
TESTED_DIGEST: ${{ steps.provenance.outputs.tested_digest }}
|
||||
BUILD_RUN_ID: ${{ steps.provenance.outputs.build_run_id }}
|
||||
PROVENANCE_TOKEN: ${{ github.token }}
|
||||
GIT_WRITE_KEY: ${{ secrets.WWW_RELEASE_KEY }}
|
||||
run: 'set -euo pipefail
|
||||
GIT_WRITE_KEY: ${{ secrets.WWW_DEPLOY_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
VERSION=$(bash ci/scripts/calver-release.sh)
|
||||
VERSION=$(bash ci/scripts/calver-release.sh www)
|
||||
TAG_NAME="services/www-$VERSION"
|
||||
|
||||
[[ "$VERSION" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]
|
||||
echo "Tagging Git commit $TESTED_SHA with $TAG_NAME..."
|
||||
bash ci/scripts/calver-release.sh tag www "$TESTED_SHA" "$VERSION"
|
||||
|
||||
if git show-ref --verify --quiet "refs/tags/$VERSION"; then exit 1; fi
|
||||
echo "Promoting Docker image to $VERSION and latest..."
|
||||
docker buildx imagetools create --prefer-index=false \
|
||||
"git.ermnvldmr.com/${{ github.repository }}/www@$TESTED_DIGEST" \
|
||||
--tag "git.ermnvldmr.com/${{ github.repository }}/www:$VERSION" \
|
||||
--tag "git.ermnvldmr.com/${{ github.repository }}/www:latest"
|
||||
|
||||
docker buildx imagetools create --prefer-index=false "git.ermnvldmr.com/deytenit/ermnvldmr.com/www@$TESTED_DIGEST"
|
||||
--tag "git.ermnvldmr.com/deytenit/ermnvldmr.com/www:$VERSION"
|
||||
|
||||
test "$(docker buildx imagetools inspect "git.ermnvldmr.com/deytenit/ermnvldmr.com/www:$VERSION"
|
||||
--format ''{{.Manifest.Digest}}'')" = "$TESTED_DIGEST"
|
||||
|
||||
git tag "$VERSION" "$TESTED_SHA"
|
||||
|
||||
python3 ci/scripts/git_write.py "refs/tags/$VERSION"
|
||||
|
||||
export VERSION
|
||||
test "$(docker buildx imagetools inspect "git.ermnvldmr.com/${{ github.repository }}/www:$VERSION" --format '{{.Manifest.Digest}}')" = "$TESTED_DIGEST"
|
||||
|
||||
export VERSION TAG_NAME
|
||||
mkdir -p .ci
|
||||
|
||||
python3 - <<''RELEASE_PROVENANCE''
|
||||
|
||||
python3 - <<'RELEASE_PROVENANCE'
|
||||
import json, os
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
Path(''.ci/www-release.json'').write_text(json.dumps({''sha'': os.environ[''TESTED_SHA''],
|
||||
''digest'': os.environ[''TESTED_DIGEST''], ''run_id'': os.environ[''BUILD_RUN_ID''],
|
||||
''repository'': ''deytenit/ermnvldmr.com'', ''release_tag'': os.environ[''VERSION'']}))
|
||||
|
||||
Path('.ci/www-release.json').write_text(json.dumps({
|
||||
'sha': os.environ['TESTED_SHA'],
|
||||
'digest': os.environ['TESTED_DIGEST'],
|
||||
'run_id': os.environ['BUILD_RUN_ID'],
|
||||
'repository': os.environ['GITHUB_REPOSITORY'],
|
||||
'release_tag': os.environ['TAG_NAME'],
|
||||
'version': os.environ['VERSION']
|
||||
}))
|
||||
RELEASE_PROVENANCE
|
||||
|
||||
'
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: www-release-provenance
|
||||
|
||||
+73
-25
@@ -14,11 +14,11 @@ IMAGE = SERVER.removeprefix('https://') + '/' + REPO + '/www'
|
||||
|
||||
def validate_input(sha, digest, run_id):
|
||||
if not re.fullmatch(r'[0-9a-f]{40}', sha):
|
||||
raise ValueError('Full source SHA required')
|
||||
raise ValueError(f'Full source SHA required: {sha}')
|
||||
if not re.fullmatch(r'sha256:[0-9a-f]{64}', digest):
|
||||
raise ValueError('OCI sha256 digest required')
|
||||
if not re.fullmatch(r'[1-9][0-9]*', run_id):
|
||||
raise ValueError('Numeric build run ID required')
|
||||
raise ValueError(f'OCI sha256 digest required: {digest}')
|
||||
if not re.fullmatch(r'[1-9][0-9]*', str(run_id)):
|
||||
raise ValueError(f'Numeric build run ID required: {run_id}')
|
||||
|
||||
|
||||
def validate_run(run, sha):
|
||||
@@ -40,21 +40,21 @@ def api(path):
|
||||
|
||||
|
||||
def validate_labels(labels, sha, run_id):
|
||||
if labels.get('org.opencontainers.image.revision') != sha or labels.get('com.ermnvldmr.ci.run') != run_id:
|
||||
if labels.get('org.opencontainers.image.revision') != sha or labels.get('com.ermnvldmr.ci.run') != str(run_id):
|
||||
raise ValueError('Immutable image provenance mismatch')
|
||||
|
||||
|
||||
def validate_manifest(manifest, sha, digest, run_id):
|
||||
expected = {'sha': sha, 'digest': digest, 'run_id': run_id, 'repository': REPO}
|
||||
if manifest != expected:
|
||||
raise ValueError('Build artifact does not attest requested image digest')
|
||||
expected = {'sha': sha, 'digest': digest, 'run_id': str(run_id), 'repository': REPO}
|
||||
if any(str(manifest.get(k)) != str(v) for k, v in expected.items()):
|
||||
raise ValueError(f'Build artifact does not attest requested image digest: got {manifest}, expected {expected}')
|
||||
|
||||
|
||||
def artifact_manifest(run_id, name='www-provenance', filename='www-provenance.json'):
|
||||
artifacts = api('/actions/runs/' + run_id + '/artifacts')['artifacts']
|
||||
artifacts = api('/actions/runs/' + str(run_id) + '/artifacts')['artifacts']
|
||||
matching = [a for a in artifacts if a.get('name') == name and not a.get('expired', True)]
|
||||
if len(matching) != 1:
|
||||
raise ValueError('Exactly one unexpired build provenance artifact required')
|
||||
raise ValueError(f'Exactly one unexpired build provenance artifact required for {name} on run {run_id}, found {len(matching)}')
|
||||
class SameHostRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
if not newurl.startswith(SERVER + '/'):
|
||||
@@ -75,8 +75,13 @@ def artifact_manifest(run_id, name='www-provenance', filename='www-provenance.js
|
||||
|
||||
|
||||
def main():
|
||||
sha, digest, run_id = os.environ['TESTED_SHA'], os.environ['TESTED_DIGEST'], os.environ['BUILD_RUN_ID']
|
||||
validate_input(sha, digest, run_id)
|
||||
sha = os.environ.get('TESTED_SHA', '').strip()
|
||||
digest = os.environ.get('TESTED_DIGEST', '').strip()
|
||||
run_id = os.environ.get('BUILD_RUN_ID', '').strip()
|
||||
release_id = os.environ.get('RELEASE_RUN_ID', '').strip()
|
||||
release_tag = os.environ.get('RELEASE_TAG', '').strip()
|
||||
is_deploy = os.environ.get('IS_DEPLOY', 'false').lower() == 'true' or bool(release_id or release_tag)
|
||||
|
||||
current = api('/actions/runs/' + os.environ['GITHUB_RUN_ID'])
|
||||
if current.get('event') != 'workflow_dispatch' or current.get('head_branch') != 'next':
|
||||
raise ValueError('Approval must be manually dispatched on protected next')
|
||||
@@ -85,28 +90,71 @@ def main():
|
||||
raise ValueError('Only the repository owner may approve promotion')
|
||||
if not api('/branches/next').get('protected'):
|
||||
raise ValueError('The next branch must be protected before promotion')
|
||||
|
||||
if is_deploy:
|
||||
if not release_id:
|
||||
runs_data = api('/actions/workflows/services-www-release-manual.yml/runs?branch=next&limit=10')
|
||||
candidates = [
|
||||
r for r in runs_data.get('workflow_runs', [])
|
||||
if r.get('status') == 'completed' and r.get('conclusion') == 'success'
|
||||
]
|
||||
if not candidates:
|
||||
raise ValueError('No successful completed run found for services-www-release-manual.yml on next')
|
||||
release_id = str(candidates[0]['id'])
|
||||
|
||||
release = api('/actions/runs/' + release_id)
|
||||
release_actor = release.get('trigger_actor') or release.get('actor') or {}
|
||||
if release.get('conclusion') != 'success' or release.get('status') != 'completed' or release.get('event') != 'workflow_dispatch' or release.get('head_branch') != 'next' or release.get('path') != 'services-www-release-manual.yml@refs/heads/next' or release_actor.get('login') != 'deytenit':
|
||||
raise ValueError('Successful independently approved release required')
|
||||
|
||||
rel_manifest = artifact_manifest(release_id, 'www-release-provenance', 'www-release.json')
|
||||
if not release_tag:
|
||||
release_tag = rel_manifest['release_tag']
|
||||
if not sha:
|
||||
sha = rel_manifest['sha']
|
||||
if not digest:
|
||||
digest = rel_manifest['digest']
|
||||
if not run_id:
|
||||
run_id = str(rel_manifest['run_id'])
|
||||
|
||||
expected_rel = {'sha': sha, 'digest': digest, 'run_id': str(run_id), 'repository': REPO, 'release_tag': release_tag}
|
||||
if any(str(rel_manifest.get(k)) != str(v) for k, v in expected_rel.items()):
|
||||
raise ValueError(f'Release approval does not match requested artifact: got {rel_manifest}, expected {expected_rel}')
|
||||
else:
|
||||
if not sha or not digest or not run_id:
|
||||
runs_data = api('/actions/workflows/services-www-next-required.yml/runs?branch=next&limit=10')
|
||||
candidates = [
|
||||
r for r in runs_data.get('workflow_runs', [])
|
||||
if r.get('status') == 'completed' and r.get('conclusion') == 'success'
|
||||
]
|
||||
if not candidates:
|
||||
raise ValueError('No successful completed run found for services-www-next-required.yml on next')
|
||||
build_run = candidates[0]
|
||||
run_id = str(build_run['id'])
|
||||
sha = build_run['head_sha']
|
||||
build_manifest = artifact_manifest(run_id, 'www-provenance', 'www-provenance.json')
|
||||
digest = build_manifest['digest']
|
||||
|
||||
validate_input(sha, digest, run_id)
|
||||
validate_run(api('/actions/runs/' + run_id), sha)
|
||||
validate_manifest(artifact_manifest(run_id), sha, digest, run_id)
|
||||
if os.environ.get('RELEASE_RUN_ID'):
|
||||
release_id = os.environ['RELEASE_RUN_ID']
|
||||
if not re.fullmatch(r'[1-9][0-9]*', release_id):
|
||||
raise ValueError('Numeric release run ID required')
|
||||
release = api('/actions/runs/' + release_id)
|
||||
actor = release.get('trigger_actor') or release.get('actor') or {}
|
||||
if release.get('conclusion') != 'success' or release.get('status') != 'completed' or release.get('event') != 'workflow_dispatch' or release.get('head_branch') != 'next' or release.get('path') != 'services-www-release-manual.yml@refs/heads/next' or actor.get('login') != 'deytenit':
|
||||
raise ValueError('Successful independently approved release required')
|
||||
manifest = artifact_manifest(release_id, 'www-release-provenance', 'www-release.json')
|
||||
expected = {'sha': sha, 'digest': digest, 'run_id': run_id, 'repository': REPO, 'release_tag': os.environ['RELEASE_TAG']}
|
||||
if manifest != expected:
|
||||
raise ValueError('Release approval does not match requested artifact')
|
||||
|
||||
auth = base64.b64encode(('deytenit:' + os.environ['PROVENANCE_TOKEN']).encode()).decode()
|
||||
subprocess.run(['git', '-c', 'http.https://git.ermnvldmr.com/.extraheader=AUTHORIZATION: basic ' + auth, 'fetch', 'origin', 'next'], check=True)
|
||||
subprocess.run(['git', 'merge-base', '--is-ancestor', sha, 'origin/next'], check=True)
|
||||
subprocess.run(['docker', 'pull', IMAGE + '@' + digest], check=True)
|
||||
data = json.loads(subprocess.check_output(['docker', 'image', 'inspect', IMAGE + '@' + digest], text=True))
|
||||
validate_labels(data[0]['Config'].get('Labels') or {}, sha, run_id)
|
||||
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as stream:
|
||||
stream.write('short_sha=sha-' + sha[:7] + '\n')
|
||||
stream.write(f'short_sha=sha-{sha[:7]}\n')
|
||||
stream.write(f'tested_sha={sha}\n')
|
||||
stream.write(f'tested_digest={digest}\n')
|
||||
stream.write(f'build_run_id={run_id}\n')
|
||||
if release_tag:
|
||||
stream.write(f'release_tag={release_tag}\n')
|
||||
if release_id:
|
||||
stream.write(f'release_run_id={release_id}\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in new issue
Block a user