Repository navigation
publish.yml: optional static-asset upload before the image push - #9
Merged
Merged
Conversation
New optional inputs static-assets-path and static-assets-destination. When set, the publish job reads that directory from the validated local OCI layout (layers applied in order, OCI whiteouts honored, links and special files rejected, size and count limits) without executing caller content, and uploads it to the GCS destination with the publisher's access token before the image is pushed. Uploads are create-only (ifGenerationMatch=0). An existing object must match byte for byte and in serving metadata, otherwise the job fails. Text assets are stored gzip-encoded; all objects get "public, max-age=31536000, immutable". Use case: content-hashed web assets (for example Next.js .next/static with assetPrefix) stay available for every build, so rolling deploys cannot serve HTML that references chunks missing from the other pods. Both inputs empty: no behaviour change.
added 3 commits
November 14, 2026 10:20
- Gate the step on the raw inputs and fail closed when the prepare outputs are missing (GitHub drops outputs that contain a secret value, which would otherwise skip the upload and push the image anyway). - Name the colliding object in the error; README: separate bucket or prefix per trust level, and how to recover from a collision. - Own AssetError for the step's messages; every other error is reported generically, so build-controlled text never reaches the log. - Reject PAX sparse maps before tarfile parses them. - On a gzip md5 mismatch with matching content hash, compare the decompressed stored object (bounded) instead of failing, so a runner zlib change cannot block unchanged assets. - Cancel queued uploads after the first failure. - Publish job timeout 35 minutes; document amd64-only asset upload.
…assets Second review round: - Include Solaris extended headers (type X) in the 1 MiB extension-header limit and cap the total of global PAX headers per layer at 1 MiB. - Fail closed at startup if the private tarfile hooks the limits rely on no longer exist. - Reject static assets with more than one platform: separate builds can produce different asset names and only one platform would be uploaded. - README: one-line Cache-Control.
- Publish job timeout 35 minutes only when static assets are used; other callers keep 20. - An existing object with Content-Disposition or Content-Language is a collision. - static-assets-path rejects all control characters. - README: whole-image parsing limits apply to every layer.
There was a problem hiding this comment.
🔵 Needs a closer look
Custom parsing of caller-controlled OCI layers and GCS upload handling in a credentialed job warrant human security review and real-runner/bucket validation beyond the local harness.
No open issues 🎉
What changed in this PR
Adds optional static-asset uploads to the reusable publish workflow so content-hashed assets remain available across rolling deployments. Uploads complete before image publishing, while callers that omit the new inputs retain existing behavior.
Changes:
- Adds paired asset-path and GCS-destination inputs, with validation and single-platform enforcement.
- Extracts assets from verified OCI layers and uploads them with create-only writes, collision checks, retries, and resource limits.
- Documents configuration, permissions, retention requirements, and a Next.js example.
| File | Description |
|---|---|
| README.md | Documents static-asset setup, safety constraints, and usage. |
| .github/workflows/publish.yml | Adds input validation, bounded asset extraction, and GCS uploads before image publishing. |
Review effort: Balanced
msokolovx
approved these changes
Nov 14, 2026
This was referenced Nov 14, 2026
DeviaVir
added a commit
that referenced
this pull request
Nov 14, 2026
…11) * Merge pull request #6 from Blockstream/security-hardening publish.yml: security fixes for explicit tags, artifact hand-off and OCI checks * publish.yml: optional static-asset upload before the image push (#9) * publish.yml: optional static-asset upload before the image push New optional inputs static-assets-path and static-assets-destination. When set, the publish job reads that directory from the validated local OCI layout (layers applied in order, OCI whiteouts honored, links and special files rejected, size and count limits) without executing caller content, and uploads it to the GCS destination with the publisher's access token before the image is pushed. Uploads are create-only (ifGenerationMatch=0). An existing object must match byte for byte and in serving metadata, otherwise the job fails. Text assets are stored gzip-encoded; all objects get "public, max-age=31536000, immutable". Use case: content-hashed web assets (for example Next.js .next/static with assetPrefix) stay available for every build, so rolling deploys cannot serve HTML that references chunks missing from the other pods. Both inputs empty: no behaviour change. * publish.yml: harden static-asset upload after security review - Gate the step on the raw inputs and fail closed when the prepare outputs are missing (GitHub drops outputs that contain a secret value, which would otherwise skip the upload and push the image anyway). - Name the colliding object in the error; README: separate bucket or prefix per trust level, and how to recover from a collision. - Own AssetError for the step's messages; every other error is reported generically, so build-controlled text never reaches the log. - Reject PAX sparse maps before tarfile parses them. - On a gzip md5 mismatch with matching content hash, compare the decompressed stored object (bounded) instead of failing, so a runner zlib change cannot block unchanged assets. - Cancel queued uploads after the first failure. - Publish job timeout 35 minutes; document amd64-only asset upload. * publish.yml: bound all tar extension headers; single-platform static assets Second review round: - Include Solaris extended headers (type X) in the 1 MiB extension-header limit and cap the total of global PAX headers per layer at 1 MiB. - Fail closed at startup if the private tarfile hooks the limits rely on no longer exist. - Reject static assets with more than one platform: separate builds can produce different asset names and only one platform would be uploaded. - README: one-line Cache-Control. * publish.yml: final review items for static-asset upload - Publish job timeout 35 minutes only when static assets are used; other callers keep 20. - An existing object with Content-Disposition or Content-Language is a collision. - static-assets-path rejects all control characters. - README: whole-image parsing limits apply to every layer. --------- Co-authored-by: chase (AI) <[email protected]> * build(deps): bump sigstore/cosign-installer (#8) Bumps the actions group with 1 update: [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer). Updates `sigstore/cosign-installer` from 4.1.2 to 4.1.3 - [Release notes](https://github.com/sigstore/cosign-installer/releases) - [Commits](sigstore/cosign-installer@6f9f177...756fe2d) --- updated-dependencies: - dependency-name: sigstore/cosign-installer dependency-version: 4.1.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: chase (AI) <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New optional inputs for
publish.yml:When both are set, the publish job uploads that directory from the built image to GCS before it pushes the image. Use case: content-hashed web assets (for example Next.js
.next/staticwithassetPrefix) stay available for every build. During a rolling deploy, a browser can no longer get HTML from one build and then a 404 for a chunk that only the other build's pods have.Both inputs empty (the default): the step is skipped, with no behaviour change.
Design
.., links and special files in the path or its ancestors, sparse entries, zstd layers, and oversized extension headers.ifGenerationMatch=0).public, max-age=31536000, immutable.prepare. The token is never printed. TLS is verified; redirects and ambient proxies are refused. Errors do not echo transport or tar-controlled text.storage.objects.create, plusstorage.objects.getfor the existing-object comparison (a publicly readable bucket covers that).Validation
publish.ymlat run time: 62 tests pass on Python 3.12 and 3.13. It covers:actionlint: clean.zizmor --offline: no findings.Release
After merge: release PR
main→v1, tagv1.2.0.Security review
Three independent read-only review rounds of this PR:
Xand global PAX header limits, multi-platform assets, privatetarfilehooks.Content-DispositionandContent-Languageare now compared.The harness now has 83 tests, all passing on Python 3.12 and 3.13.
actionlintis clean, andzizmor --offlinehas no findings.