Skip to content

ci(release): version with release-please and publish a container image - #529

Open
svalleru wants to merge 2 commits into
mainfrom
feat/release-pipeline
Open

ci(release): version with release-please and publish a container image#529
svalleru wants to merge 2 commits into
mainfrom
feat/release-pipeline

Conversation

@svalleru

Copy link
Copy Markdown

Summary

  • What. release-please (node type) keeps a release PR open and tags
    vX.Y.Z; a Publish workflow builds that commit's Dockerfile for
    linux/amd64 and pushes
    us-docker.pkg.dev/e2b-artifacts/dashboard/dashboard:vX.Y.Z through workload
    identity.
  • Why. Self-hosted installs need a version to pin, and a commit SHA is the
    only identity this repository has today. The
    E2B Embed package
    pins the dashboard image the way it pins the others.
  • The release job uses the same GitHub App token the spec sync uses, ungated.
    A tag pushed with the default GITHUB_TOKEN does not trigger on: push, so a
    missing App must fail loudly rather than produce a release that never
    publishes.
  • The publisher is unreachable from a pull request. Publish holds
    id-token: write but runs only on a v* tag push and manual dispatch, and
    tags are immutable. Attestations are off because Artifact Registry rejects
    buildx's default manifest lists.
  • Seeded at 0.1.0 with bump-minor-pre-major, so the first release is
    v0.2.0.
    No release PR opens until the first feat or fix after the
    bootstrap commit.

Verification

  • 254 unit tests pass. New ones pin both sides of the tag contract
    (include-component-in-tag: false and the workflow's
    ^v[0-9]+\.[0-9]+\.[0-9]+$), the registry path and platform, the identity
    variables, and the absence of pull_request, pull_request_target and
    workflow_run.
  • actionlint clean on both workflows; no existing workflow fires on tag
    pushes, so a release starts exactly one run.
  • The build was rehearsed locally against the container PR's Dockerfile with
    --load instead of --push: a 111 MB linux/amd64 image with the expected
    labels.
  • Untestable before a real release: the identity exchange, the registry push,
    and whether the App grants contents: write.

Merge order / notes

  • Merge the container PR first, since this workflow builds its Dockerfile, and
    the runtime-config PR before the first tag, or the image bakes
    NEXT_PUBLIC_E2B_DOMAIN=unset.invalid. All three PRs edit README.md and
    package.json in disjoint regions.
  • Set GCP_WORKLOAD_IDENTITY_PROVIDER and GCP_SERVICE_ACCOUNT as repository
    variables before the first tag; until then the first step fails with a FIX
    line naming both.
  • The container PR's workflow header says nothing else in CI builds the image.
    Publish now does — adjust it in whichever of the two merges second.

@cla-bot cla-bot Bot added the cla-signed label Sep 12, 2026
@svalleru
svalleru marked this pull request as ready for review September 12, 2026 01:58

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I checked whether workflow_dispatch's ref and tag inputs could be used to publish a tag that doesn't actually match the ref being built — they are independent free-text fields with no cross-check against real git tags, but triggering it already requires repo write/dispatch access, so this is an operational footgun rather than a new attack surface and doesn't change the outcome here.

Extended reasoning...

I reproduced the ruled-out candidate by reading .github/workflows/publish.yml: in the "Resolve the tag and the commit" step, tag comes from inputs.tag on workflow_dispatch while the checkout step uses inputs.ref || github.ref — these are two separate free-text inputs with no validation that a git tag named tag exists or points at the commit resolved from ref. This confirms the previously ruled-out item is real but bounded: workflow_dispatch on this workflow can only be triggered by someone with write access to the repository (GitHub restricts manual dispatch to collaborators with write permission), so it does not introduce a fork-reachable or privilege-escalation path beyond what a trusted maintainer could already do by hand — it's a usability/footgun issue (a maintainer typo could publish v1.2.3 built from an unrelated ref) rather than a security hole, consistent with why it was ruled out rather than confirmed. Since two other findings (the tag-existence guard treating any gcloud failure as "free", and mutable @ v3 action tags for the GCP auth chain) are already confirmed and will be posted inline, and this additional item is genuinely something I verified beyond those, the narrow exception for a brief defer note applies.

Comment on lines +96 to +106
- name: Set up gcloud
uses: google-github-actions/setup-gcloud@v3

- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker us-docker.pkg.dev --quiet

- name: Refuse to overwrite an existing tag
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
set -euo pipefail

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) The tag-existence guard swallows every gcloud artifacts docker tags list failure (auth/IAM error, transient API error, quota, wrong project) as "tag is free", not just the NOT_FOUND-before-first-publish case the comment describes, so a misconfigured or under-permissioned publish service account silently disables the overwrite check on every run instead of failing loudly. Fix: only treat an actual NOT_FOUND/repository-missing error as "free"; any other list failure should fail the step instead of falling through to existing="".

Extended reasoning...

Step 'Refuse to overwrite an existing tag' does if ! existing="$(gcloud artifacts docker tags list ...)"; then existing=""; fi — any nonzero exit from gcloud (not just the documented pre-first-publish NOT_FOUND) is coerced to an empty tag list, so the subsequent grep -Fxq never matches and the guard never fires. If the publish service account is granted registry write but not artifactregistry.reader, or a transient API error occurs, this happens on every run, not just before the first image — the workflow proceeds to build/push believing no prior tag exists, losing the intended early, readable failure the PR's own comment claims this step provides.

Verification: nit. The mechanism the candidate cites is real and reachable. In publish.yml lines 118-121, if ! existing="$(gcloud artifacts docker tags list ... 2>&1)"; then ... existing=""; fi coerces ANY nonzero gcloud exit (missing artifactregistry.reader, transient API error, quota, wrong project) into an empty tag list, so the grep -Fxq -- "${TAG}" <<<"${existing}" on line 122 never matches…

} >> "$GITHUB_OUTPUT"

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) google-github-actions/auth@ v3 (and setup-gcloud@ v3, docker/setup-buildx-action@ v3, plus release-please.yml's create-github-app-token@ v2 and googleapis/release-please-action@ v4) are pinned to mutable major-version tags, not immutable commit SHAs. This diff is the first workflow in the repo to pair that convention with real cloud credentials: id-token:write plus GCP workload identity is new to CI here, so a maintainer of any of those actions repointing the tag (compromise or mistake) now runs with write access to the registry, whereas today no workflow in this repo exposes cloud credentials to an unpinned action. …

Extended reasoning...

…Fix: pin every third-party action used in publish.yml and release-please.yml to a full commit SHA (with a version comment), which covers all six sites (publish.yml:64,91,97,128; release-please.yml:38,44).

A tag push triggers publish.yml, which calls google-github-actions/auth@ v3 to exchange OIDC for a short-lived GCP access token scoped to the workload-identity-bound service account, then uses that token to push to Artifact Registry. If the upstream v3 tag is ever repointed to malicious code (supply-chain compromise or accidental force-push by the action's maintainers), the next tag push on this repo executes that code with id-token:write and the publish service account's registry-push credentials, and separately release-please.yml exchanges a GitHub App private key for a token with contents:write/pull-requests:write via an unpinned create-github-app-token@ v2 and hands it to an unpinned release-please-action@ v4. No other workflow in this repo currently exposes credentials this sensitive to a mutable action reference, so this PR is what newly creates that blast radius, not a…

Verification: nit (supply-chain hardening). The code is exactly as described: publish.yml pins google-github-actions/auth@ v3 (line 91), setup-gcloud@ v3 (97), docker/setup-buildx-action@ v3 (128), actions/checkout@ v4 (64); release-please.yml pins create-github-app-token@ v2 (38) and googleapis/release-please-action@ v4 (44) — all mutable major-version tags, no commit SHAs. And the credential surface is…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant