morseDOCS

Publish from CI

Push to main, get a revision on chain. A separate pipeline key you can revoke, the environment variables the CLI reads, and a workflow file to copy.

The idea

Your posts live in a git repo. On every push to main, CI publishes new files as entries and changed files as revisions. No deploy step, no server to keep up: the published state is on Sui and Walrus, the source of truth is the repo, and the map file links the two.

A CI-only publisher key

Do not put your owner key in CI. Make a separate wallet for the pipeline, fund it with a little SUI and WAL, and issue it a PublisherCap from your owner wallet. If CI ever leaks the key, you revoke one cap and move on.

bashon your machine
morse cap issue 0xCI_WALLET_ADDRESS
morse --json publication get | jq -r .id      # you will need this id

Environment variables the CLI reads

In a non-interactive shell the CLI takes everything from the environment. Flags beat variables, variables beat the config file.

VariableSet it toWhy
MORSE_PRIVATE_KEYThe CI wallet's suiprivkey1...Highest-priority key source, never written to disk
MORSE_NETWORKtestnetNo profile needed
MORSE_PUBLICATIONThe publication object idActive publication without morse use
MORSE_COLLECTIONpostsActive collection

With those four set, morse status in CI shows the same context you have locally, with no keystore and no prompts.

GitHub Actions workflow

This reuses import.sh from Import an existing Markdown site and commits the updated map back to the repo so the next run knows what exists.

yaml.github/workflows/publish.yml
name: Publish to Morse
on:
  push:
    branches: [main]
    paths: ["content/posts/**", "morse-map.json"]

permissions:
  contents: write

jobs:
  publish:
    runs-on: ubuntu-latest
    env:
      MORSE_PRIVATE_KEY: ${{ secrets.MORSE_CI_PRIVATE_KEY }}
      MORSE_NETWORK: testnet
      MORSE_PUBLICATION: ${{ vars.MORSE_PUBLICATION_ID }}
      MORSE_COLLECTION: posts
      EPOCHS: "30"
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm i -g @arcadiasystems/morse-cli
      - run: morse --yes -q status
      - run: ./import.sh
      - name: Commit the map
        run: |
          git config user.name "morse-ci"
          git config user.email "ci@users.noreply.github.com"
          git add morse-map.json
          git diff --cached --quiet || git commit -m "chore: update morse map" && git push

Store the key as a repository secret and the publication id as a repository variable. The paths filter keeps the job from running on unrelated commits.

Idempotency and exit codes

The map is what makes a rerun safe: a file already in it is skipped. Everything else you need for scripting is in the CLI's exit codes and --json.

Exit codeMeaningIn a pipeline
0SuccessCarry on
1Any other failureFor example a contract abort such as ESlugAlreadyExists, or a write that uploaded to Walrus and then failed on chain; read the message and fail the job
2Usage errorYour command line is wrong, fix the script
3Not foundWrong publication id or entry id
4Auth or permissionCap revoked, wrong key, or no cap for this publication. A revoked cap on entry add or entry add-encrypted exits 1 instead, after the upload
5Network or transportRetry once after a pause, then fail the job
bashretry once on transport errors
publish() { morse --json -q entry add "$@"; }
out=$(publish "$name" --file "$f" --epochs "$EPOCHS") || { [ $? -eq 5 ] && sleep 20 && out=$(publish "$name" --file "$f" --epochs "$EPOCHS"); }
--yes and -q
--yes answers every confirmation, which matters for delete commands and cap revoke; it goes before the subcommand. -q keeps progress bars out of your logs so the JSON on stdout is the only thing the script sees.

Lock it down

RiskMitigation
Key leaks from CIIt is a publisher key, not the owner. morse cap revoke and rotate.
Someone pushes garbage to mainBranch protection, and the chain keeps every revision so nothing is lost
Pipeline publishes twiceThe map, committed back after every run
WAL runs outCheck the CI wallet balance in the job and fail early with a clear message

Next