Skip to content

CI and CD

Four ways to let a build read what it needs, in decreasing order of trust.

A build needs plaintext. That is the whole problem: everything CodeSeal does to keep source unreadable has to be undone, briefly, somewhere, by something you trust.

The four modes differ in who that something is, and how long they hold the key. sgit init --ci-mode <mode> writes the matching workflow.

modewho decryptswho sees plaintext
secretsyour CI runneryour CI provider
portal-keyyour CI runner, with a 60-second single-use keyyour CI provider
portal-builda Portal workeryour Portal host
portal-dispatchyour CI runner, per buildyour CI provider

Mode 1 — secrets

The simplest. Key material lives in GitHub Secrets, the runner reads it, decrypts, builds, cleans up.

  1. The runner checks out ciphertext

    An ordinary actions/checkout. What lands is blobs.

  2. sgit is installed

    With SGIT_ONLY=1, so the installer fetches the binary and nothing else.

  3. The key is injected from GitHub Secrets

    As an environment variable, never an argument.

  4. run-secure decrypts, builds, cleans

    sgit run-secure -- ./build.sh. Plaintext is removed afterwards whether the build passed or failed.

Choose it when your CI provider is already trusted with everything else, and you want the encryption to protect you against storage exposure rather than against the build.

Mode 2 — portal-key

The runner never holds a long-lived key. It presents a bootstrap secret, gets a single-use token with a sixty-second life, redeems it for a wrapped runner key, unwraps it, and deletes it before the build starts.

  1. Exchange the bootstrap secret for a token

    sgit ci request-token. The Portal verifies the secret against a stored hash and mints a token that lives sixty seconds in Redis.

  2. Redeem the token for a wrapped key

    sgit ci runner-key. Consumed atomically, so a replay finds it already spent.

  3. Unwrap locally, then delete both keys

    sgit ci unwrap-key, then rm — before build.sh runs. The Portal stores the key encrypted to a wrapper public key it does not hold, so it cannot read what it hands out.

  4. run-secure decrypts, builds, cleans

    sgit run-secure -- ./build.sh, the same finale as Mode 1.

the four calls, in order
sgit ci request-token > .ci-token
sgit ci runner-key > runner.wrapped.age
sgit ci unwrap-key -i wrapper.age -I runner.wrapped.age -o runner.key
rm -f runner.wrapped.age wrapper.age   # gone before the build starts
sgit run-secure -- ./build.sh

Every message this exchanges is drawn out, arrow by arrow, on the CI/CD page.

Choose it when you want central audit and rotation — someone can see which builds requested keys and when — while builds stay on your existing CI.

Mode 3 — portal-build

Your CI provider never sees plaintext at all. It asks for a build; the Portal's worker clones, decrypts, builds and returns an artifact.

  1. The runner posts a commit SHA

    sgit build trigger. That is all it sends, and all it knows.

  2. A Portal worker does the work

    Clone, decrypt with the admin key, delete the keys, run build.sh.

  3. The artifact is uploaded

    The runner polls sgit build status until the build reaches a terminal state, then collects the artifact.

Choose it when your CI provider is explicitly outside the trust boundary. Note what you have done, though: the Portal host is now inside it. This moves the trust, it does not remove it.

Mode 4 — portal-dispatch

The most involved, and the one that keeps the Portal's existence out of your CI configuration.

  1. Your push notifies the Portal

    sgit push sends the commit SHA when auto_on_push is set. Note the direction — your machine talks to the Portal, and the workflow never does.

  2. The Portal mints a token bound to that commit

    Encrypted to the dispatch public key in the manifest, so only the runner holding the matching private key can open it.

  3. The Portal calls GitHub

    workflow_dispatch, with the encrypted token as an input. This is the whole point of the mode: the connection opens outward, so your workflow needs no Portal URL and no Portal credentials.

  4. The runner unwraps and builds

    sgit ci unwrap-dispatch-token, then run-secure. The token was valid for one commit and is now spent.

triggering by hand, when you do not want auto_on_push
sgit ci dispatch --commit-sha $(git rev-parse HEAD)

Because sgit push dispatches, the Portal sees every change — the point if you need an audit trail that cannot be sidestepped by pushing from somewhere else.

The reversed trigger direction is the part worth seeing rather than reading: the diagram makes it obvious in a way this paragraph does not.

Choose it when you need per-build scoping and a complete audit trail, and builds must stay on your existing CI.

Which one

Most teams should start at secrets and move only when they can say what they are moving away from.

The honest ordering by what each protects against:

  • Storage exposure at your Git host — all four, equally. This is what CodeSeal is for.
  • A compromised CI provider — only portal-build.
  • A long-lived key sitting in CI configurationportal-key and portal-dispatch.
  • Not knowing what was built from whatportal-dispatch.

If the answer is only the first one, secrets is the right mode and the others are cost without benefit.

See also