Skip to content

Commit ba379e1

Browse files
committed
chore(ci): initial pre-release workflow
1 parent 6f5c763 commit ba379e1

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed

.github/workflows/pre-release.yml

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
name: Release
2+
3+
# RELEASE PROCESS
4+
#
5+
# === Automated activities ===
6+
#
7+
# 1. [Seal] Bump to release version and export source code with integrity hash
8+
# 2. [Quality check] Restore sealed source code, run tests, linting, security and complexity base line
9+
# 3. [Build] Restore sealed source code, create and export hashed build artifact for PyPi release (wheel, tarball)
10+
# 4. [Provenance] Generates provenance for build, signs attestation with GitHub OIDC claims to confirm it came from this release pipeline, commit, org, repo, branch, hash, etc.
11+
# 5. [Release] Restore built artifact, and publish package to PyPi prod repository
12+
# 6. [PR to bump version] Restore sealed source code, and create a PR to update trunk with latest released project metadata
13+
14+
# NOTE
15+
#
16+
# See MAINTAINERS.md "Releasing a new version" for release mechanisms
17+
#
18+
# Every job is isolated and starts a new fresh container.
19+
20+
env:
21+
RELEASE_COMMIT: ${{ github.sha }}
22+
23+
on:
24+
workflow_dispatch:
25+
inputs:
26+
skip_code_quality:
27+
description: "Skip tests, linting, and baseline. Only use if release fail for reasons beyond our control and you need a quick release."
28+
default: false
29+
type: boolean
30+
required: false
31+
skip_pypi:
32+
description: "Skip publishing to PyPi. Used for testing release steps."
33+
default: false
34+
type: boolean
35+
required: false
36+
37+
38+
permissions:
39+
contents: read
40+
41+
jobs:
42+
43+
# This job bumps the package version to the pre-release version
44+
# creates an integrity hash from the source code
45+
# uploads the artifact with the integrity hash as the key name
46+
# so subsequent jobs can restore from a trusted point in time to prevent tampering
47+
seal:
48+
runs-on: ubuntu-latest
49+
permissions:
50+
contents: read
51+
outputs:
52+
integrity_hash: ${{ steps.seal_source_code.outputs.integrity_hash }}
53+
artifact_name: ${{ steps.seal_source_code.outputs.artifact_name }}
54+
RELEASE_VERSION: ${{ steps.release_version.outputs.RELEASE_VERSION }}
55+
steps:
56+
- name: Export release version
57+
id: release_version
58+
run: |
59+
RELEASE_VERSION="$(poetry version prerelease --short | head -n1 | tr -d '\n')"
60+
61+
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT"
62+
63+
- name: Verifies pre-release version semantics
64+
# verify pre-release semantics before proceeding to avoid versioning pollution
65+
# e.g., 2.40.0a1 and 2.40.0b2 are valid while 2.40.0 is not
66+
run: |
67+
if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+[a-b].*$ ]]; then
68+
echo "Version $VERSION doesn't look like a pre-release version; aborting"
69+
exit 1
70+
fi
71+
env:
72+
RELEASE_VERSION: ${{ steps.release_version.outputs.RELEASE_VERSION}}
73+
74+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
75+
with:
76+
ref: ${{ env.RELEASE_COMMIT }}
77+
78+
# We use a pinned version of Poetry to be certain it won't modify source code before we create a hash
79+
- name: Install poetry
80+
run: |
81+
pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0
82+
pipx inject poetry git+https://github.com/monim67/poetry-bumpversion@315fe3324a699fa12ec20e202eb7375d4327d1c4 # v0.3.1
83+
84+
- name: Bump package version
85+
id: versioning
86+
run: poetry version "${RELEASE_VERSION}"
87+
env:
88+
RELEASE_VERSION: ${{ steps.release_version.outputs.RELEASE_VERSION}}
89+
90+
- name: Seal and upload
91+
id: seal_source_code
92+
uses: ./.github/actions/seal
93+
with:
94+
artifact_name_prefix: "source"
95+
96+
# This job runs our automated test suite, complexity and security baselines
97+
# it ensures previously merged have been tested as part of the pull request process
98+
#
99+
# NOTE
100+
#
101+
# we don't upload the artifact after testing to prevent any tampering of our source code dependencies
102+
quality_check:
103+
needs: seal
104+
runs-on: ubuntu-latest
105+
permissions:
106+
contents: read
107+
steps:
108+
# NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev)
109+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
110+
with:
111+
ref: ${{ env.RELEASE_COMMIT }}
112+
113+
- name: Restore sealed source code
114+
uses: ./.github/actions/seal-restore
115+
with:
116+
integrity_hash: ${{ needs.seal.outputs.integrity_hash }}
117+
artifact_name: ${{ needs.seal.outputs.artifact_name }}
118+
119+
- name: Debug cache restore
120+
run: cat pyproject.toml
121+
122+
- name: Install poetry
123+
run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0
124+
- name: Set up Python
125+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
126+
with:
127+
python-version: "3.12"
128+
cache: "poetry"
129+
- name: Install dependencies
130+
run: make dev
131+
- name: Run all tests, linting and baselines
132+
run: make pr
133+
134+
# This job creates a release artifact (tar.gz, wheel)
135+
# it checks out code from release commit for custom actions to work
136+
# then restores the sealed source code (overwrites any potential tampering)
137+
# it's done separately from release job to enforce least privilege.
138+
# We export just the final build artifact for release
139+
build:
140+
runs-on: ubuntu-latest
141+
needs: [quality_check, seal]
142+
permissions:
143+
contents: read
144+
outputs:
145+
integrity_hash: ${{ steps.seal_build.outputs.integrity_hash }}
146+
artifact_name: ${{ steps.seal_build.outputs.artifact_name }}
147+
attestation_hashes: ${{ steps.encoded_hash.outputs.attestation_hashes }}
148+
steps:
149+
# NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev)
150+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
151+
with:
152+
ref: ${{ env.RELEASE_COMMIT }}
153+
154+
- name: Restore sealed source code
155+
uses: ./.github/actions/seal-restore
156+
with:
157+
integrity_hash: ${{ needs.seal.outputs.integrity_hash }}
158+
artifact_name: ${{ needs.seal.outputs.artifact_name }}
159+
160+
- name: Install poetry
161+
run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0
162+
- name: Set up Python
163+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
164+
with:
165+
python-version: "3.12"
166+
cache: "poetry"
167+
168+
- name: Build python package and wheel
169+
run: poetry build
170+
171+
- name: Seal and upload
172+
id: seal_build
173+
uses: ./.github/actions/seal
174+
with:
175+
artifact_name_prefix: "build"
176+
files: "dist/"
177+
178+
# NOTE: SLSA retraces our build to its artifact to ensure it wasn't tampered
179+
# coupled with GitHub OIDC, SLSA can then confidently sign it came from this release pipeline+commit+branch+org+repo+actor+integrity hash
180+
- name: Create attestation encoded hash for provenance
181+
id: encoded_hash
182+
working-directory: dist
183+
run: echo "attestation_hashes=$(sha256sum ./* | base64 -w0)" >> "$GITHUB_OUTPUT"
184+
185+
# This job creates a provenance file that describes how our release was built (all steps)
186+
# after it verifies our build is reproducible within the same pipeline
187+
# it confirms that its own software and the CI build haven't been tampered with (Trust but verify)
188+
# lastly, it creates and sign an attestation (multiple.intoto.jsonl) that confirms
189+
# this build artifact came from this GitHub org, branch, actor, commit ID, inputs that triggered this pipeline, and matches its integrity hash
190+
# NOTE: supply chain threats review (we protect against all of them now): https://slsa.dev/spec/v1.0/threats-overview
191+
provenance:
192+
needs: [seal, build]
193+
permissions:
194+
contents: write # nested job explicitly require despite upload assets being set to false
195+
actions: read # To read the workflow path.
196+
id-token: write # To sign the provenance.
197+
# NOTE: provenance fails if we use action pinning... it's a Github limitation
198+
# because SLSA needs to trace & attest it came from a given branch; pinning doesn't expose that information
199+
# https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/generic/README.md#referencing-the-slsa-generator
200+
uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
201+
with:
202+
base64-subjects: ${{ needs.build.outputs.attestation_hashes }}
203+
upload-assets: false # we upload its attestation in create_tag job, otherwise it creates a new release
204+
205+
# This job uses release artifact to publish to PyPi
206+
# it exchanges JWT tokens with GitHub to obtain PyPi credentials
207+
# since it's already registered as a Trusted Publisher.
208+
# It uses the sealed build artifact (.whl, .tar.gz) to release it
209+
release:
210+
needs: [build, seal, provenance]
211+
environment: release
212+
runs-on: ubuntu-latest
213+
permissions:
214+
id-token: write # OIDC for PyPi Trusted Publisher feature
215+
env:
216+
RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }}
217+
steps:
218+
# NOTE: we need actions/checkout in order to use our local actions (e.g., ./.github/actions)
219+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
220+
with:
221+
ref: ${{ env.RELEASE_COMMIT }}
222+
223+
- name: Restore sealed source code
224+
uses: ./.github/actions/seal-restore
225+
with:
226+
integrity_hash: ${{ needs.build.outputs.integrity_hash }}
227+
artifact_name: ${{ needs.build.outputs.artifact_name }}
228+
229+
- name: Upload to PyPi prod
230+
if: ${{ !inputs.skip_pypi }}
231+
uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14
232+
233+
# Creates a PR with the latest version we've just released
234+
# since our trunk is protected against any direct pushes from automation
235+
bump_version:
236+
needs: [release, seal]
237+
permissions:
238+
contents: write # create-pr action creates a temporary branch
239+
pull-requests: write # create-pr action creates a PR using the temporary branch
240+
runs-on: ubuntu-latest
241+
steps:
242+
# NOTE: we need actions/checkout to authenticate and configure git first
243+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
244+
with:
245+
ref: ${{ env.RELEASE_COMMIT }}
246+
247+
- name: Restore sealed source code
248+
uses: ./.github/actions/seal-restore
249+
with:
250+
integrity_hash: ${{ needs.seal.outputs.integrity_hash }}
251+
artifact_name: ${{ needs.seal.outputs.artifact_name }}
252+
253+
- name: Create PR
254+
id: create-pr
255+
uses: ./.github/actions/create-pr
256+
with:
257+
files: "pyproject.toml aws_lambda_powertools/shared/version.py"
258+
temp_branch_prefix: "ci-bump"
259+
pull_request_title: "chore(ci): new pre-release ${{ needs.seal.outputs.RELEASE_VERSION
260+
}}"
261+
github_token: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)