Static sites

Any repository that builds a static site, or a browser app that is nothing but files, can ship it to your own hosting account with one reviewed toolkit. It is the same promise as the rest of CLANKER.NET: it runs in your repository, with your credentials, and we hold none of them.

The toolkit does four things and nothing else:

StepWhat it does
validateChecks your contract before anything is installed or any credential is read.
sealAfter your own build and checks pass, records a digest of every file, bound to the commit, the environment and the contract.
credentialsLoads the hosting token from your stored configuration, masks it, and never writes it to an output.
publishChecks every target and seal before the first upload, uploads the sealed files without rebuilding, then runs your smoke checks.

It does not create projects, attach domains, touch DNS or apply infrastructure. A declared project that does not exist stops the release instead of being created. Domain and DNS changes stay in a separate step that you confirm by hand.

Five minutes

1. Say what is published. A static block in the repository’s one config file, replicator.config.ts, reviewed with the rest of your release workflow:

export default replicator.app({
  name: "my-site",
  state: { accountId: "<32 hex digits>" },
  stages: { production: {} },
  static: {
    adapter: "vite",
    buildCommands: ["npm run build"],
    apiBoundary: "None: browser-only artifact.",
    environments: {
      production: { baseUrl: "https://example.com" },
      preview: {},
    },
    targets: [
      {
        key: "site",
        outputDirectory: "dist",
        artifactName: "site-dist",
        smokeCommand: 'node scripts/smoke.mjs "$STATIC_DEPLOY_URL"',
        environments: {
          production: { project: "my-site", productionBranch: "main" },
          preview: { project: "my-site", productionBranch: "main" },
        },
      },
    ],
  },
  run(ctx) {
    new replicator.StaticSite("site", {
      accountId: "<32 hex digits>",
      project: "my-site",
      productionBranch: "main",
      domains: ["example.com"],
    });
  },
});

The toolkit reads a small contract file; there is none to keep. The engine writes it from the block, adding what it already knows: the app’s name, the state account, the credential variable (REPLICATOR_STATE_TOKEN), the toolkit’s version and the pinned upload tool. The release ref defaults to the first target’s production branch.

replicator infra static-contract --config replicator.config.ts --out contract.json

It exits 3 when the config has no static block, and refuses a static block in an app with no state.accountId. The contract is rebuilt in every job from the same config, so the bytes a seal is bound to are the bytes publish checks.

The three generic example contracts shipped with the toolkit (packages/replicator/deploy/examples/) show a plain Vite site, a site plus a separate app bundle, and a site whose app shares one artifact with the public site. They name placeholder projects and domains: the toolkit holds no repository’s contract. The contract your repository builds from its static block has the same shape, and your Pages projects and domains are declared beside it in the same replicator.config.ts (the StaticSite component).

  • adapter names how your build runs (vite, astro or astro-expo); the toolkit never runs a build command from this file. buildCommands documents what your own workflow runs.
  • Every project is declared in your repository’s replicator config; publishing never creates one, so a missing project stops the release.
  • smokeCommand is reviewed code from your repository, run with STATIC_DEPLOY_URL and STATIC_DEPLOY_TARGET. Never fill it from a dispatch input or from pull-request text.
  • outputDirectory must stay inside your repository.
  • Each run reads absolute origins from STATIC_SITE_BASE_URL (and STATIC_SITE_ORIGIN / STATIC_SITE_APP_ORIGIN when the contract requires them). The 1.x fallback variables, bare hostnames, URL prefixes and a target’s id were removed in 2.0.0; a contract that names one is refused.

2. Call the four steps from your release workflow. Build and check with your own commands, then build the contract (the job needs the engine: setup-replicator) and call the steps:

- uses: clankerlabs/CLANKERNET/.github/actions/static-site@<40-character commit>
  with: { operation: validate, config: contract.json }
# … your build and checks …
- uses: clankerlabs/CLANKERNET/.github/actions/static-site@<40-character commit>
  with: { operation: seal, config: contract.json }
- uses: clankerlabs/CLANKERNET/.github/actions/static-site@<40-character commit>
  with: { operation: credentials, config: contract.json }
- uses: clankerlabs/CLANKERNET/.github/actions/static-site@<40-character commit>
  with: { operation: publish, config: contract.json }

REPLICATOR_STATE_TOKEN is the one secret the workflow needs: the token that opens your state also publishes the sites, and the account id beside it names where the projects live; both come from state.accountId in the config, so they are written once.

The workflow then needs REPLICATOR_STATE_TOKEN as its one secret and no other tooling is installed for the credentials step. The sites themselves, their domains and their storage can be declared in the repository’s environments file.

Take a project’s name from your environments file. When the repository’s environments file owns a project, the contract does not have to repeat its name. Write the name of the output that carries it instead:

"production": { "project": { "fromOutput": "publicProject" }, "productionBranch": "main" }

and pass the file the deploy wrote (replicator infra deploy --outputs-out outputs.json) as the step’s outputs input. validate and seal only check the shape, so they run without it; publish reads the name from the file and then runs the usual checks on that project. Renaming a project in the environments file is then a change in one place. A release that deploys the environment first and publishes second (the release pipeline) does this for you.

3. Pin it. The reference is a full 40-character commit, never a branch or a moving tag, so a release of yours is always built by a toolkit you reviewed. The contract’s toolkitVersion must match the toolkit’s version, so a mismatch fails validate before any credential is read.

What it protects

  • Nothing is rebuilt at publish time. The seal binds the bytes that passed your checks to this commit, this environment, this canonical address and this exact contract; publishing refuses anything else.
  • Pull requests never publish. A preview is a deliberate, branch-scoped upload; production needs the declared release ref.
  • All or nothing at the start. Every target and every seal is checked before the first byte is uploaded, and smoke checks run only after all uploads (an app and its site are compared once both are updated).
  • A receipt for every release (static-site-receipt.json): the resource identifiers, the immutable addresses and the previous deployment of each target.
  • No automatic rollback. After a partial multi-project upload, read the receipt and redeploy the previous commit, or pick the recorded previous deployments through the path that owns the resource. Keep the previous toolkit pin, contract and workflow together as one rollback unit.

Reaching the toolkit

Nothing is copied into your repository. Use the action from a checkout of this repository at a pinned commit:

  1. Check ours out at a pinned commit next to yours (for example into .factory/, with a read token if the repository is private) and use the action from that checkout: uses: ./.factory/.github/actions/static-site. The action finds the package relative to itself and exports its path as STATIC_DEPLOY_TOOLKIT for your own shell adapters. This is the same pattern the release pipeline uses for the engine.
  2. Reference the action directly at a pinned commit, as above, when your repository may use actions from the one that hosts the toolkit.
  3. Call the reusable release workflow (replicator-release.yml), which does both for you.

Limits

  • One kind of hosting target today (a static-site project with a production branch). The target shape is where another kind would be added; nothing else in the contract changes.
  • The toolkit needs Node 22 and no packages.