-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaction.yml
More file actions
351 lines (341 loc) · 17.8 KB
/
Copy pathaction.yml
File metadata and controls
351 lines (341 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
name: "Deploy to DIG Network"
description: "Publish your build to the DIG Network on Chia — git-push-to-deploy. Installs the digstore CLI, runs `digstore deploy`, and reports the capsule + URLs + cost back on the PR."
author: "DIG Network"
branding:
icon: "upload-cloud"
color: "green"
inputs:
directory:
description: "The built-output directory to publish (your dist/ or build/)."
required: false
default: "dist"
store-id:
description: "The 64-hex store id to advance. Optional — defaults to `store-id` in your dig.toml."
required: false
default: ""
if-changed:
description: "Skip the deploy (and the $DIG + XCH spend) when the build is byte-identical to the live version. The no-op guard that lets you run this on every push."
required: false
default: "true"
preview:
description: >-
Force a PR-style preview even off the default branch. Wave-2 (#18) free, no-spend per-PR
previews are NOT YET available — until that infra ships, `preview: true` would publish a REAL
capsule on Chia (a $DIG spend) merely labelled "preview". To avoid a silent spend, this action
FAILS CLOSED when `preview: true` is set unless you also set `allow-paid-preview: true`. PRs
already preview automatically (event-derived, never blocked). Leave false for production deploys.
required: false
default: "false"
allow-paid-preview:
description: >-
Opt in to the paid `preview: true` path while free no-spend previews (#18) are unavailable.
Required ONLY when you set `preview: true`; without it a forced preview fails closed rather than
silently publishing a real ($DIG) capsule. Has no effect on the automatic event-based preview.
required: false
default: "false"
digstore-version:
description: >-
The digstore CLI version to install. A release tag (e.g. `v0.6.0`), a git ref/branch, or
`latest`. PIN this for reproducible CI. The keyless writer deploy-key (`--writer-key`) and the
free `deploy --preview` path (#17/#18) require digstore >= `v0.6.0`, which is the default.
required: false
default: "v0.6.0"
keyless:
description: >-
KEYLESS CI auth (#23): obtain a GitHub OIDC token (audience=dighub) and exchange it with the
hub for a short-lived, store-scoped session — NO long-lived dighub secret in the repo. Requires
`permissions: id-token: write`. The repo+ref must be bound to your store on hub.dig.net. On by
default; set `false` only for the legacy secret-based path.
required: false
default: "true"
api-base:
description: "The dighub control-plane API base for the OIDC exchange. Defaults to the public hub."
required: false
default: "https://hub.dig.net/v1"
writer-key:
description: >-
The on-chain WRITER DELEGATE key (64-hex seed) that advances the store's root WITHOUT the owner
master seed (#17). The owner pre-authorizes this writer (hub Teams "Deployer"); it can change
ONLY the metadata root — never the owner, never melt. Pass from a repo secret. Maps to
`digstore deploy --writer-key` / `DIGSTORE_WRITER_KEY`.
required: false
default: ""
passphrase:
description: >-
The deploy wallet's DIGSTORE_PASSPHRASE — unlocks the seed that PAYS the on-chain fee ($DIG +
XCH) for a real deploy to the default branch. NOT needed for PR previews (free, no chain). In
keyless mode the writer-key advances the root and the OIDC session authorizes the push; this
seed only funds the spend. Use a DEDICATED deploy wallet funded with only what your deploys
need — never your main wallet. Pass from a repo SECRET only.
required: false
default: ""
deploy-key:
description: >-
The store's §21 PUBLISHER deploy key (64-hex, from `digstore deploy-key export`). Authorizes
publishing the capsule head to DIGHub; has NO on-chain spend authority. Distinct from
`writer-key` (the on-chain root-advance authority). Pass from a repo secret. Maps to
`digstore deploy --deploy-key` / `DIGSTORE_DEPLOY_KEY`.
required: false
default: ""
mnemonic:
description: "The deploy wallet's BIP-39 mnemonic, imported under the passphrase before deploy. Pass from a repo secret. Has full spend authority — use a dedicated deploy wallet (see `passphrase`)."
required: false
default: ""
salt:
description: "Secret salt (64-hex) for a PRIVATE store. Omit for public stores. Pass from a repo secret."
required: false
default: ""
remote:
description: "The remote to publish to (e.g. `dig://<store-id>` or a self-hosted node URL). Defaults to the public DIGHub."
required: false
default: ""
message:
description: "Commit message for the new capsule. Defaults to the commit being deployed."
required: false
default: ""
build-command:
description: "Optional shell command to build before deploying (e.g. `npm ci && npm run build`). Usually you build in a prior step instead."
required: false
default: ""
wait-timeout:
description: "Seconds to wait for on-chain confirmation (0 = submit and don't block)."
required: false
default: "600"
comment-on-pr:
description: "On a pull request, upsert a comment with the capsule, URLs and cost, and set a GitHub deployment + commit status."
required: false
default: "true"
github-token:
description: "Token used to comment on the PR and create the deployment/commit status. Defaults to the workflow's GITHUB_TOKEN."
required: false
default: "${{ github.token }}"
working-directory:
description: "Directory to run digstore from (where dig.toml lives). Defaults to the repo root."
required: false
default: "."
outputs:
capsule:
description: "The published capsule: `storeId:rootHash`."
value: ${{ steps.report.outputs.capsule }}
root:
description: "The new on-chain root hash."
value: ${{ steps.report.outputs.root }}
store-id:
description: "The store id that was advanced."
value: ${{ steps.report.outputs.store-id }}
chia-url:
description: "The chia:// content-open address of the deployment (rootless = latest tip) — the user-facing scheme the DIG Browser/extension register."
value: ${{ steps.report.outputs.chia-url }}
dig-url:
description: "DEPRECATED alias of `chia-url` (now also a chia:// content-open address). Use `chia-url`."
value: ${{ steps.report.outputs.dig-url }}
urn:
description: "The root-pinned URN permalink (urn:dig:chia:<store>:<root>)."
value: ${{ steps.report.outputs.urn }}
hub-url:
description: "The DIGHub URL for the store (https://hub.dig.net/stores/<id>)."
value: ${{ steps.report.outputs.hub-url }}
coin-id:
description: "The on-chain coin id of the anchored root."
value: ${{ steps.report.outputs.coin-id }}
content-address:
description: "On a `--preview` build: the shareable root-pinned chia:// content-open address of the preview capsule. Empty on a real deploy."
value: ${{ steps.report.outputs.content-address }}
preview:
description: "`true` when this run produced a free preview build (PR), not a real on-chain deploy."
value: ${{ steps.report.outputs.preview }}
skipped:
description: "`true` when --if-changed skipped a no-op deploy."
value: ${{ steps.report.outputs.skipped }}
spent:
description: "`true` when the deploy spent $DIG (a real publish)."
value: ${{ steps.report.outputs.spent }}
pushed:
description: "`true` when the capsule was published to the hub."
value: ${{ steps.report.outputs.pushed }}
json:
description: >-
The whole normalized deploy result as ONE JSON blob (incl. the `outcome`). Parse this once
(`JSON.parse(steps.dig.outputs.json)`) instead of re-stitching the scalar outputs.
value: ${{ steps.report.outputs.json }}
outcome:
description: >-
The catalogued result: one of success | skipped | preview | dry-run | anchor-failed |
push-failed | timed-out | no-credential | unauthorized | oidc-error | blocked-paid-preview |
failed. Branch on this instead of scraping log lines. Written even on the failure path.
value: ${{ steps.report.outputs.outcome }}
failure-reason:
description: "A human/machine reason string when `outcome` is a failure; empty otherwise."
value: ${{ steps.report.outputs.failure-reason }}
environment:
description: "The resolved deploy environment: `preview` or `production` (from the event mode)."
value: ${{ steps.mode.outputs.environment }}
teardown:
description: >-
`true` when this run tore down a closed PR's preview (deactivated its GitHub Deployment(s) and
updated the PR comment) instead of building — a closed `pull_request` event. `false` otherwise.
value: ${{ steps.mode.outputs.teardown }}
runs:
using: "composite"
steps:
# Decide PR-preview vs push-deploy vs PR-close-teardown from the triggering
# event (pure JS, tested). On pull_request → a free preview (or, when the PR
# just CLOSED, a teardown — no build); on push/dispatch to the default
# branch → a real deploy; anything else → preview (never a surprise spend
# off a branch).
- name: Decide deploy mode (preview vs deploy vs teardown)
id: mode
shell: bash
# Pass the event via DIG_* (a step `env:` cannot override the reserved
# GITHUB_EVENT_NAME / GITHUB_REF the runner sets); mode.mjs prefers DIG_*.
# DIG_EVENT_ACTION carries `github.event.action` (e.g. "opened"/"closed") —
# GitHub does not expose it as a reserved GITHUB_* var itself.
env:
ACTION_PATH: ${{ github.action_path }}
DIG_EVENT_NAME: ${{ github.event_name }}
DIG_EVENT_ACTION: ${{ github.event.action }}
DIG_REF: ${{ github.ref }}
DIG_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
DIG_FORCE_PREVIEW: ${{ inputs.preview }}
DIG_ALLOW_PAID_PREVIEW: ${{ inputs.allow-paid-preview }}
run: |
# Fails closed (exit 1) when `preview: true` would silently spend without
# `allow-paid-preview: true` — before any install/deploy/spend happens.
node "$ACTION_PATH/src/mode.mjs"
# A real (non-preview) deploy needs a wallet seed to PAY the on-chain fee. A
# preview is free, so it needs no credential — only previews may run without one.
- name: "Guard: a real deploy needs a funding credential"
if: steps.mode.outputs.preview != 'true' && inputs.passphrase == ''
shell: bash
run: |
echo "::error::no funding credential for a real deploy: set 'passphrase' (the deploy wallet's DIGSTORE_PASSPHRASE) as a repo secret. PR previews are free and need none."
exit 1
# A PR-close teardown never builds — skip the install (and every step below,
# down to the deploy) entirely so closing a PR costs nothing and installs nothing.
- name: Install digstore CLI (pinned)
if: steps.mode.outputs.teardown != 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github-token }}
IN_DIGSTORE_VERSION: ${{ inputs.digstore-version }}
ACTION_PATH: ${{ github.action_path }}
run: |
bash "$ACTION_PATH/scripts/install-digstore.sh" "$IN_DIGSTORE_VERSION"
# KEYLESS auth (#23): exchange the job's GitHub OIDC token (audience=dighub) for
# a short-lived store-scoped dighub session, written to DIG_IDENTITY_DIR so the
# §21 head push is authorized WITHOUT a static secret. Skipped for a preview
# (free, no hub push) and when keyless is disabled. NEVER prints any token.
- name: Keyless dighub auth (GitHub OIDC exchange)
id: keyless
if: inputs.keyless == 'true' && steps.mode.outputs.preview != 'true'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
DIG_API_BASE: ${{ inputs.api-base }}
DIG_IDENTITY_DIR: ${{ runner.temp }}/dig-identity
# The runner injects ACTIONS_ID_TOKEN_REQUEST_URL / _TOKEN into the step env
# automatically when the job has `permissions: id-token: write`; auth.mjs reads
# them directly (no need to re-map them here).
run: |
node "$ACTION_PATH/src/auth.mjs"
- name: Import deploy wallet (if a mnemonic is provided)
if: inputs.mnemonic != '' && steps.mode.outputs.preview != 'true'
shell: bash
env:
DIGSTORE_PASSPHRASE: ${{ inputs.passphrase }}
DIG_MNEMONIC: ${{ inputs.mnemonic }}
working-directory: ${{ inputs.working-directory }}
run: |
# Restore the wallet seed from the secret mnemonic, unlocked by the
# passphrase. `--mnemonic` is the non-interactive form (no TTY prompt).
digstore seed import --mnemonic "$DIG_MNEMONIC"
- name: Deploy to DIG
id: deploy
if: steps.mode.outputs.teardown != 'true'
shell: bash
# All inputs are passed through `env:` and read as $VARS in the script
# (never interpolated as `${{ }}` into the run body), so an input value can
# never inject shell — the script-injection-safe pattern.
env:
# The keyless session (and the dighub API base) digstore reads for the push.
DIG_IDENTITY_DIR: ${{ runner.temp }}/dig-identity
DIG_API_BASE: ${{ inputs.api-base }}
DIGSTORE_PASSPHRASE: ${{ inputs.passphrase }}
DIGSTORE_DEPLOY_KEY: ${{ inputs.deploy-key }}
DIGSTORE_WRITER_KEY: ${{ inputs.writer-key }}
DIGSTORE_STORE_SALT: ${{ inputs.salt }}
IN_PREVIEW: ${{ steps.mode.outputs.preview }}
IN_DIRECTORY: ${{ inputs.directory }}
IN_IF_CHANGED: ${{ inputs.if-changed }}
# Prefer the store the OIDC exchange bound; fall back to the input / dig.toml.
IN_STORE_ID: ${{ steps.keyless.outputs.store-id || inputs.store-id }}
IN_REMOTE: ${{ inputs.remote }}
IN_MESSAGE: ${{ inputs.message }}
IN_BUILD_COMMAND: ${{ inputs.build-command }}
IN_WAIT_TIMEOUT: ${{ inputs.wait-timeout }}
ACTION_PATH: ${{ github.action_path }}
working-directory: ${{ inputs.working-directory }}
run: |
set -o pipefail
# The preview-vs-real-deploy arg mapping is unit-tested (src/deploy-args.mjs);
# this just loads its NUL-separated argv into a bash array (safe against any
# value containing spaces/newlines, e.g. `message`).
mapfile -d '' -t args < <(node "$ACTION_PATH/src/print-deploy-args.mjs")
# `--non-interactive` + `--yes` are GLOBAL digstore flags; deploy is
# already non-interactive in --json mode, but pass them so any nested
# confirmation auto-approves on a runner with no TTY.
out_file="${RUNNER_TEMP:-/tmp}/dig-deploy.json"
echo "Running: digstore --non-interactive --yes ${args[*]}"
digstore --non-interactive --yes "${args[@]}" | tee "$out_file"
echo "json-file=$out_file" >> "$GITHUB_OUTPUT"
- name: Report (outputs + PR comment + deployment status)
id: report
# ALWAYS run (for a build/deploy run) so a catalogued `outcome` + `failure-reason` is written
# even on the failure path (a guard/auth/deploy step aborting before any JSON). report.mjs
# handles an empty input by emitting the failure outcome from the DIG_PRIOR_* hints below.
# Skipped on a PR-close teardown — there is no deploy JSON to report on (see the teardown step).
if: always() && steps.mode.outputs.teardown != 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github-token }}
DIG_COMMENT_ON_PR: ${{ inputs.comment-on-pr }}
# The mode decision is authoritative (a PR auto-previews); the CLI's own
# `preview` flag in the JSON still wins inside report.mjs.
DIG_PREVIEW: ${{ steps.mode.outputs.preview }}
DIG_DEPLOY_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
ACTION_PATH: ${{ github.action_path }}
DEPLOY_JSON_FILE: ${{ steps.deploy.outputs.json-file }}
# Pre-deploy failure context, mapped to the catalogued outcome enum so report.mjs can emit
# the CAUSE (not just "failed") when there is no deploy JSON:
# - the spend guard fired (forced paid preview without opt-in),
# - the funding-credential guard fired (a real deploy with no passphrase),
# - keyless OIDC exchange failed (401 audience / 403 unbound repo).
DIG_PRIOR_OUTCOME: >-
${{ steps.mode.outputs.paid-preview-blocked == 'true' && 'blocked-paid-preview'
|| (steps.mode.outcome == 'failure' && 'failed')
|| (steps.keyless.outcome == 'failure' && 'oidc-error')
|| (steps.deploy.outcome == 'failure' && 'failed')
|| '' }}
DIG_PRIOR_REASON: >-
${{ steps.mode.outputs.paid-preview-blocked == 'true'
&& 'preview: true would publish a real (paid) capsule; set allow-paid-preview: true to opt in'
|| (steps.keyless.outcome == 'failure' && 'keyless OIDC exchange failed (check id-token permission + repo→store binding)')
|| (steps.deploy.outcome == 'failure' && 'digstore deploy failed — see the deploy step log')
|| 'the deploy failed before producing a result' }}
run: |
node "$ACTION_PATH/src/report.mjs" "$DEPLOY_JSON_FILE"
# PR-close teardown (roadmap #18): the PR just closed, so its preview no longer
# represents anything live. Deactivate every GitHub Deployment this action created
# for that PR (src/github.mjs deactivateDeployments, matched via the deployment
# payload across ALL of the PR's pushes — not just the last one) and replace the
# PR's deploy comment with a closed notice. Never runs digstore; never spends;
# always best-effort (a closing PR must never be blocked by cleanup cosmetics).
- name: Tear down PR preview (mark deployment(s) inactive)
if: always() && steps.mode.outputs.teardown == 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github-token }}
DIG_COMMENT_ON_PR: ${{ inputs.comment-on-pr }}
ACTION_PATH: ${{ github.action_path }}
run: |
node "$ACTION_PATH/src/teardown.mjs"