Skip to content

feat(v2): publish the IAP sign-in page from the deployed image - #433

Draft
mattdrees wants to merge 4 commits into
pipeline-v2from
feat/deploy-publishes-signin-page
Draft

feat(v2): publish the IAP sign-in page from the deployed image#433
mattdrees wants to merge 4 commits into
pipeline-v2from
feat/deploy-publishes-signin-page

Conversation

@mattdrees

@mattdrees mattdrees commented Jul 28, 2026

Copy link
Copy Markdown
Member

Matt: this is an idea I want to chat about with Brian and team sometime. Doesn't need immediate feedback.

Claude:

Summary

Makes the deploy action publish the IAP friendly sign-in page out of the image
it is deploying, for apps that have one (iap.friendly_signin on the
gcp/cloudrun/app module).

Today the app repo owns that upload. That does not survive v2:

  • Production has no automated path. v2 promotes an already-built image, so no
    app-repo workflow fires on a prod deploy. Today's upload is a manual dispatch —
    bills' workflow says so in its header comment.
  • Rollback puts an old app behind a new page. The page inlines the app's own
    CSS, so it is version-specific, but nothing tied it to the deployed digest.

So the page travels inside the image and the deploy publishes it. Same
artifact, same digest, same guarantees as the app: promote and rollback carry the
matching page for free.

Secondary benefit worth naming: this bucket is the one world-readable,
unauthenticated surface of an otherwise-gated app, sitting on the app's origin
directly in front of the sign-in flow. The module README already treats "a bucket
the app's CI can write" as a phishing surface. This narrows the writers from any
app-repo workflow dispatch
to the deploy path only.

Contract with the app image

LABEL org.cru.iap-signin=signin
COPY --from=builder /app/tmp/static-export/signin /cru/iap-signin/signin

The label value is the GCS object key and is the single source of truth for it,
so image and bucket cannot disagree about the object name.

Detection is bucket-first. Terraform already injects IAP_SIGNIN_BUCKET into
the app container, so the deploy reads it off the pre-update service spec. No new
action input, no app-info field, and no registry read at all for the apps
that have no sign-in page.

Extraction reads one blob, not the image. src/v2/oci.js speaks just enough
of the Docker Registry v2 API to read the config blob (labels) and then scan
layers newest-first for the file, stopping at the first hit. src/v2/tar.js is a
minimal ustar reader rather than a new dependency — one fixed short path is all
it has to handle.

Verified against a real bills image built with this change: found on the first
of 19 layers, 87KB fetched vs a 346MB image
, byte-identical to the built page.

Failure is never fatal. The page is static, cosmetic and pre-auth — the module
README calls a few minutes of staleness a non-outage — so failing a production
promote over it would trade a real problem for a trivial one. The publish runs
last (a deploy that fails partway must not leave a new page in front of an old
app) and anything that goes wrong becomes a ::warning:: annotation. A missing
label warns rather than errors: that is the expected state when rolling back to a
release built before this contract existed.

Companion PRs

  • cru-terraform-modules#698 (the IAP module PR, already draft) — grants the
    per-env cru-deploy SA objectAdmin on the sign-in bucket. Merge before
    this lands anywhere real
    , or the publish step warns on every deploy.
  • bills — Dockerfile builds + labels the page, and drops its upload workflow.

The v1 grant and the IAP_SIGNIN_BUCKET Actions variable stay in the module for
now; beacon still uploads its own page on v1. Both come out when beacon migrates.

Decisions I made that are worth a second opinion

  1. Warn, don't fail. A publish failure leaves the run green with a warning
    annotation. The alternative — failing the deploy — makes prod skew impossible
    but lets a cosmetic page block a promote. I went with warn; say the word and
    it becomes fatal.
  2. No Slack plumbing yet. deployCloudRun returns the publish result but the
    deploy action does not expose it as an output, so a skipped publish shows up
    only as a run annotation, not in the promote/rollback Slack message. Noted as
    a follow-up in docs/pipeline-v2.md rather than expanding this diff across
    three workflows.
  3. Hand-rolled tar reader. ~90 lines instead of a tar-stream dependency in
    a shared actions repo. It skips PAX/GNU long-name records rather than
    interpreting them, which is safe for a fixed 22-character path and documented
    as such. Happy to swap in the dep if you'd rather.
  4. Cloud Run only. AWS apps use aws/cloudfront/sso-static-site, so there is
    no ECS/Lambda equivalent to add.

Note on the red CI check

Lint, Build & Test fails at npm ci with a pre-existing lockfile drift on
pipeline-v2, unrelated to this branch:

Invalid: lock file's @emnapi/wasi-threads@1.2.2 does not satisfy @emnapi/wasi-threads@1.2.3
Missing: @emnapi/core@1.11.1 from lock file

Every recent pipeline-v2 commit's Build dist/ run fails the same way. I left
package-lock.json alone rather than fold an unrelated dependency fix into this
diff — say if you'd rather I regenerate it here.

Locally, against the same tree with deps installed via npm install --no-save:
371 tests pass (306 before, +65 here), npm run lint clean, npm run build
regenerates dist/.

mattdrees and others added 4 commits July 28, 2026 16:11
Enough of the Docker Registry v2 API to support publishing an artifact that
ships inside the app image. Reads the config blob for labels, then scans layers
newest-first for one known path, stopping at the first hit -- so a file COPYed
late in a Dockerfile costs one small blob fetch rather than a full image pull.
No docker daemon, no crane binary.

src/v2/tar.js is a minimal ustar reader rather than a dependency: the only
caller wants one file at a fixed, short, conventional path, and a shared
actions repo pays for every transitive dep in review surface. PAX and GNU
long-name records are skipped, not interpreted -- documented, and harmless for
a 22-character path.

authClient is exported from src/v2/gcp.js so the registry reads reuse the same
ADC credentials and scope as the Artifact Registry REST calls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With iap.friendly_signin, Terraform owns the sign-in bucket and the LB routing
but something has to own the bytes. That was a workflow in the app repo, which
does not survive v2: prod promotes an already-built image, so no app-repo
workflow fires on a prod deploy (the upload was a manual dispatch), and a
rollback re-deployed an old image behind a page built for a newer one.

So the page travels in the image and the deploy publishes it -- same digest,
same guarantees as the app. promote and rollback carry the matching page for
free, and the only identity that can write to this world-readable, pre-auth
bucket is the deployer rather than any app-repo workflow dispatch.

Contract: LABEL org.cru.iap-signin=<object key> plus the built page at
/cru/iap-signin/<object key>. The label is the single source of truth for the
key, so image and bucket cannot disagree about the object name.

Detection is bucket-first: Terraform already injects IAP_SIGNIN_BUCKET into the
app container, so its presence on the pre-update service spec is the signal.
No new action input, and no registry read for the apps that have no page.

Never fatal. The page is static, cosmetic and pre-auth -- the module README
calls a few minutes of staleness a non-outage -- so failing a production
promote over it would trade a real problem for a trivial one. Publishes last so
a partly-failed deploy cannot leave a new page in front of an old app, and
warns on anything that goes wrong. A missing label warns rather than errors:
that is the expected state when rolling back to a release built before this
contract existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant