3535 environment : release
3636 permissions :
3737 contents : write # Required for git push and tag creation
38+ id-token : write # Required for gitsign keyless Sigstore OIDC signing
3839 outputs :
3940 tag : ${{ steps.version.outputs.tag }}
4041 version : ${{ steps.version.outputs.version }}
@@ -48,10 +49,17 @@ jobs:
4849 ssh-key : ${{ secrets.DEPLOY_KEY }}
4950 persist-credentials : true
5051
52+ - name : Install gitsign (Sigstore keyless git signing)
53+ uses : chainguard-dev/actions/setup-gitsign@de68b87302e6266db5fb5220246f8aa46fe94b67 # v1.6.14
54+
5155 - name : Configure git
5256 run : |
5357 git config user.name "github-actions"
5458 git config user.email "github-actions@github.com"
59+ git config --global gpg.x509.program gitsign
60+ git config --global gpg.format x509
61+ git config --global tag.gpgsign true
62+ git config --global commit.gpgsign true
5563
5664 - name : Set up Python
5765 uses : actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -126,13 +134,13 @@ jobs:
126134 VERSION : ${{ steps.version.outputs.version }}
127135 run : |
128136 git add pyproject.toml uv.lock CITATION.cff ${CHANGELOG_FILE}
129- git commit -m "chore: release v${VERSION}"
137+ git commit -S - m "chore: release v${VERSION}"
130138
131- - name : Create and push tag
139+ - name : Create and push signed tag
132140 env :
133141 TAG : ${{ steps.version.outputs.tag }}
134142 run : |
135- git tag -a "${TAG}" -m "Release ${TAG}"
143+ git tag -s "${TAG}" -m "Release ${TAG}"
136144 git push origin HEAD
137145 git push origin "${TAG}"
138146
@@ -212,6 +220,31 @@ jobs:
212220 # is self-identifying.
213221 cp .vex/httptap.openvex.json "sbom/${PACKAGE_NAME}-${VERSION}.openvex.json"
214222
223+ - name : Generate man page
224+ env :
225+ VERSION : ${{ needs.prepare.outputs.version }}
226+ run : |
227+ mkdir -p man
228+ uv tool run --from 'argparse-manpage>=4.7' argparse-manpage \
229+ --pyfile httptap/cli.py \
230+ --function create_parser \
231+ --project-name httptap \
232+ --version "${VERSION}" \
233+ --author "Sergei Ozeranskii" \
234+ --author-email "noreply@httptap.dev" \
235+ --url "https://github.com/ozeranskii/httptap" \
236+ --manual-title "httptap" \
237+ --output "man/${PACKAGE_NAME}.1"
238+ gzip --best "man/${PACKAGE_NAME}.1"
239+
240+ - name : Upload man-page artifact
241+ uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
242+ with :
243+ name : man
244+ path : man/
245+ retention-days : 7
246+ if-no-files-found : error
247+
215248 - name : Upload SBOM artifacts
216249 uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
217250 with :
@@ -220,13 +253,40 @@ jobs:
220253 retention-days : 7
221254 if-no-files-found : error
222255
256+ publish-testpypi :
257+ name : Publish to TestPyPI
258+ runs-on : ubuntu-latest
259+ timeout-minutes : 5
260+ needs :
261+ - prepare
262+ - build
263+ environment :
264+ name : testpypi
265+ url : https://test.pypi.org/project/${{ env.PACKAGE_NAME }}/
266+ permissions :
267+ id-token : write # Required for TestPyPI OIDC trusted publishing
268+
269+ steps :
270+ - name : Download artifacts
271+ uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
272+ with :
273+ name : dist
274+ path : dist/
275+
276+ - name : Publish to TestPyPI
277+ uses : pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
278+ with :
279+ repository-url : https://test.pypi.org/legacy/
280+ attestations : true
281+
223282 publish-pypi :
224283 name : Publish to PyPI
225284 runs-on : ubuntu-latest
226285 timeout-minutes : 5
227286 needs :
228287 - prepare
229288 - build
289+ - publish-testpypi
230290 environment :
231291 name : pypi
232292 url : https://pypi.org/project/${{ env.PACKAGE_NAME }}/
@@ -242,6 +302,86 @@ jobs:
242302
243303 - name : Publish to PyPI
244304 uses : pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
305+ with :
306+ # PEP 740 attestations are signed via Sigstore OIDC and
307+ # surfaced on the PyPI project page as "Verified publisher".
308+ attestations : true
309+
310+ publish-container :
311+ name : Publish container image to GHCR
312+ runs-on : ubuntu-latest
313+ timeout-minutes : 20
314+ needs :
315+ - prepare
316+ permissions :
317+ contents : read
318+ packages : write # Push to ghcr.io/<owner>/<repo>
319+ id-token : write # Sigstore OIDC for keyless cosign signing
320+ attestations : write # GitHub build provenance attestations
321+
322+ steps :
323+ - name : Checkout tag
324+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
325+ with :
326+ ref : ${{ needs.prepare.outputs.tag }}
327+ persist-credentials : false
328+
329+ - name : Set up QEMU for multi-arch builds
330+ uses : docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
331+
332+ - name : Set up Docker Buildx
333+ uses : docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
334+
335+ - name : Log in to GitHub Container Registry
336+ uses : docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
337+ with :
338+ registry : ghcr.io
339+ username : ${{ github.actor }}
340+ password : ${{ secrets.GITHUB_TOKEN }}
341+
342+ - name : Derive container tags and labels
343+ id : meta
344+ uses : docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
345+ with :
346+ images : ghcr.io/${{ github.repository }}
347+ tags : |
348+ type=semver,pattern={{version}},value=${{ needs.prepare.outputs.tag }}
349+ type=semver,pattern={{major}}.{{minor}},value=${{ needs.prepare.outputs.tag }}
350+ type=semver,pattern={{major}},value=${{ needs.prepare.outputs.tag }}
351+ type=raw,value=latest,enable={{is_default_branch}}
352+ labels : |
353+ org.opencontainers.image.revision=${{ github.sha }}
354+ org.opencontainers.image.version=${{ needs.prepare.outputs.version }}
355+
356+ - name : Build and push image
357+ id : build
358+ uses : docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
359+ with :
360+ context : .
361+ platforms : linux/amd64,linux/arm64
362+ push : true
363+ tags : ${{ steps.meta.outputs.tags }}
364+ labels : ${{ steps.meta.outputs.labels }}
365+ provenance : mode=max
366+ sbom : true
367+ cache-from : type=gha
368+ cache-to : type=gha,mode=max
369+
370+ - name : Install cosign
371+ uses : sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
372+
373+ - name : Sign image with cosign (keyless Sigstore)
374+ env :
375+ IMAGE : ghcr.io/${{ github.repository }}
376+ DIGEST : ${{ steps.build.outputs.digest }}
377+ run : cosign sign --yes "${IMAGE}@${DIGEST}"
378+
379+ - name : Attach SLSA build provenance
380+ uses : actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
381+ with :
382+ subject-name : ghcr.io/${{ github.repository }}
383+ subject-digest : ${{ steps.build.outputs.digest }}
384+ push-to-registry : true
245385
246386 create-release :
247387 name : Create GitHub Release
@@ -253,6 +393,7 @@ jobs:
253393 - prepare
254394 - build
255395 - publish-pypi
396+ - publish-container
256397
257398 steps :
258399 - name : Checkout
@@ -272,6 +413,12 @@ jobs:
272413 name : sbom
273414 path : sbom/
274415
416+ - name : Download man-page artifact
417+ uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
418+ with :
419+ name : man
420+ path : man/
421+
275422 - name : Create GitHub Release
276423 env :
277424 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
@@ -280,7 +427,7 @@ jobs:
280427 REPO : ${{ github.repository }}
281428 run : |-
282429 gh release create "$TAG" \
283- dist/* sbom/* \
430+ dist/* sbom/* man/* \
284431 --repo "$REPO" \
285432 --title "$TAG" \
286433 --notes "$NOTES"
0 commit comments