Repository navigation
Set up GitHub Actions deploys with harper deploy setup=true provider=github-actions - #3127
Conversation
…=github-actions` A GitHub Actions workflow can deploy with its OIDC identity token instead of a stored credential, but only under a trust policy that matches its token claim for claim; a mismatch reaches CI as a bare 401. This adds the one-time setup to the existing `harper deploy setup=true` flow (HarperFast/create-harper#145): - derives the audience from the resolved target exactly as the CLI normalizes it, pins that target for the rest of the run, and takes the repository from repo= or the origin remote and its numeric id from `gh`, the public API, or repository_id= - checks the branch and environment against the workflow file in the checkout, refusing a pair no run of it could present - creates a deploy-only role and user (`<component>-ci-deploy`, with deploy_component and get_job) and the policy `github-actions-<component>`, only where missing: a matching record is left alone, and one that differs or was deactivated stops setup before it writes, so it can neither repurpose a role nor undo a revocation - reads the policy back and compares it claim for claim, then sets the HARPER_CLI_TARGET repository variable with `gh`, or exits non-zero with the command to run - needs no secret custody, so it branches before get_secrets_public_key buildRequest keeps each argument as typed on a non-enumerable `_rawArgs`, so `branch=1.0` is not pinned as "1". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the pre-push review of the GitHub Actions setup: - an existing role that sets structure_user or cluster_user no longer reads as a match - an on.push.branches pattern (`**`, `release/*`) is reported as unverified instead of as a mismatch, since GitHub matches those as globs - when the repository was not looked up (an explicit repository_id=), setup says its name casing was not verified, since workflow_ref is compared exactly - workflow= naming a directory is refused plainly instead of failing with EISDIR - the unreachable "created before the failure" catch is replaced by a line before any write saying a rerun finishes a partial setup, since cliOperations exits rather than throwing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… repository up From the second pre-push review of the GitHub Actions setup: - only a saved-login target is pinned for the run; an explicit or environment target can't change mid-run, and pinning its normalized form dropped credentials carried in its userinfo - the repository is looked up even with repository_id=, so workflow_ref uses GitHub's spelling of the name; a looked-up id that differs is refused - workflow= is normalized to the repository-relative path GitHub names, used for both the file check and the claim, and must be in .github/workflows/ - branch patterns using `+`, and a branch only workflow_dispatch can reach, are reported as unverified rather than refused - the policy list is read again just before add_oidc_trust, which replaces, so a policy created meanwhile stops setup; the residual window needs a server-side guard - an unreadable workflow file is a CLI error, and a literal `_rawArgs=` argument no longer throws Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n test an impossible repository Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nvironment From the closing pre-push review: - a workflow whose push filter leaves out the branch, but that a schedule, repository_dispatch, workflow_run or workflow_dispatch run can reach on it, is reported as unverified rather than refused - a workflow in which no job sets an environment is refused, since GitHub omits the environment claim and the policy could never match; one that calls a reusable workflow stays unverified, since the called file can set it - the module header points at security/DESIGN.md instead of repeating it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…text Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er keeps its spelling The default schema reads `branches: [1.0]` as the number 1, so comparing it as text afterwards still lost the "1.0" GitHub matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for GitHub Actions OIDC trusted publishing via the harper deploy setup=true provider=github-actions command. It adds a new module to check workflow files, reconcile roles, users, and trust policies, and configure repository variables. It also preserves raw CLI arguments to prevent numeric-looking branch names from being parsed as numbers. Feedback highlights that the fallback GitHub API fetch request in bin/deploySetupGithubActions.ts lacks a required User-Agent header, which will cause the API to reject the request with a 403 error when the gh CLI is unavailable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`harper deploy` checks the target's version with registration_info before it uploads. The role setup creates refused it, so the deploy still worked (the check assumes a current server when refused) but the cluster logged a denied operation on every CI deploy. registration_info returns only Harper's version, and any role without an operations allowlist may already call it, so the role and the policy scope now list it. Found by running a create-harper project's deploy job end to end against a local build, with a mock OIDC issuer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`deploy:setup-ci` now also grants registration_info, the version check `harper deploy` makes before it uploads (HarperFast/harper#3127). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…turns - commands.md: the role `harper deploy setup=true provider=github-actions` creates now also allows registration_info (HarperFast/harper#3127). - deploying-from-ci.mdx and operations.md: the hand-written deploy role and policy scope list it too, since `harper deploy` checks the version with it first and an allowlist that omits it logs a denial on every deploy. - operations.md: registration_info returns only `{ version, deprecated }` (since v5.0.0), not registration status, RAM or license details. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| if (plan.policy === 'create') { | ||
| // add_oidc_trust replaces a policy, so one created or disabled since the preflight would be | ||
| // overwritten. Checking again narrows that window; it can't close it without a server-side guard. | ||
| if (await policyNow()) { |
There was a problem hiding this comment.
major — A concurrent policy change can be overwritten, undoing a revocation.
There was a problem hiding this comment.
Setup can't undo a revocation. It writes a policy only when none exists, at the preflight and again just before the write; an existing policy that differs or is disabled stops setup before it writes anything. Deactivating the user also holds, because setup refuses an inactive user and add_oidc_trust rejects a policy that names one. The remaining window is a policy with the same id created by someone else in the one round trip before the write, which setup would replace. add_oidc_trust could take a no-replace flag (Table.create 409s), but hdb_oidc_trust replicates, so that check is per node and narrows the window rather than closing it. A 5.3 cluster would also ignore the flag. I've left it as documented in security/DESIGN.md, and corrected "creates or disables" to "creates" there and in the docs (abe0935, HarperFast/documentation#722).
🤖 Posted by Claude Code
…ight workflow From review: - The repository variable is now created like the cluster records: setup reads it first, keeps one that already names this cluster, and leaves one that names another cluster (exit 1, with the gh command to change it). Replacing it moved every workflow in the repository that reads it. - The local workflow file is only checked when the checkout is the repository the policy is for. Another repository's file could refuse a workflow that matches, or pass one that doesn't; it is now a note. - The workflow check requires a job in the environment to grant `permissions: id-token: write` (job-level permissions replace the workflow's; GitHub's defaults grant none). Without it the CLI can't request a token, so the policy would never be used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… token From the review round on the previous commit: - The variable is created with `gh api -X POST .../actions/variables`, which doesn't replace an existing one, instead of `gh variable set`, which does. One created between setup's read and its write is read again and judged like any other: kept if it names this cluster, left otherwise. - The value printed when a variable names another cluster has any `user:password@` removed. - When a job calls a reusable workflow, a missing `id-token: write` on the jobs in the environment is reported as unverified, not refused: the called workflow can set the environment and request the token itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t on it `admin:pass@host:9925` parses as a URL whose scheme is `admin:`, so the password survived; a pattern now drops `user:password@` with or without a scheme. withoutCredentials had also landed between variableAction and its doc comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eckout rule Follows HarperFast/harper#3127's review changes: setup leaves a HARPER_CLI_TARGET that already names another cluster, requires a job in the environment to grant `id-token: write`, and checks the local workflow only when the checkout is the repository the policy is for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Setup writes a policy only when none exists at the preflight and at the re-check just before the write, and refuses one that is disabled, so it can't undo a disable or a drop. What the last round trip can overwrite is a policy another super user creates with the same id, enabled or not. A server-side create-only write would narrow that, not close it: hdb_oidc_trust replicates, so the check would be per node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Setup never writes a policy that already exists, so another super user's disable can't be overwritten; only a policy with the same id created in the moment before setup writes it can be (HarperFast/harper#3127). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a workflow uses a trust policy (#722) * Document `harper deploy setup=true provider=github-actions`, and how a workflow uses a trust policy - commands.md: a GitHub Actions deploys (OIDC) section for the new setup provider: what it checks and creates, that it only creates, its names, and how to revoke; `setup=true` mentions it - authentication.md: a "Using it in a workflow" subsection that says plainly that nothing in a workflow names the policy, and every `harper` command in a job authenticates through it when four conditions hold, with a one-job excerpt - operations.md and the CI guide point at both, and the guide notes the setup command suits branch-triggered workflows, not its tag-triggered ones - 5.4 release notes: the new setup Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Match the setup steps to the final provider: the repository is always looked up, and what the workflow check refuses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Close the setup tip, and say authentication is not authorization From the pre-push review: - the CI guide's :::tip closing marker sat on the paragraph's last line, so the admonition never closed; it now has a line of its own - "every harper command authenticates" now says each may do only what the policy's user's role allows, and the guide notes setup's role lacks the get_deployment the manual role adds - condition 4 lists every credential that outranks the exchange, and links the precedence order - commands.md says a missing workflow file leaves the whole check unverified, that a concurrent writer can still overwrite the policy, and what to do when setting the variable fails; every page names the role setup creates Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Qualify the release note's workflow check and create-only claim, and say a rerun sets the variable again Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Note v5.4.0 in plain text where it sits mid-sentence, keeping the badge for headings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * List registration_info in the CI deploy roles, and correct what it returns - commands.md: the role `harper deploy setup=true provider=github-actions` creates now also allows registration_info (HarperFast/harper#3127). - deploying-from-ci.mdx and operations.md: the hand-written deploy role and policy scope list it too, since `harper deploy` checks the version with it first and an allowlist that omits it logs a denial on every deploy. - operations.md: registration_info returns only `{ version, deprecated }` (since v5.0.0), not registration status, RAM or license details. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep registration_info's other mentions consistent, and qualify the 5.4 note - operations.md: `get_fingerprint` no longer points at registration_info, which returns only the version. - operations-api-commands.md: registration_info gets the Harper version. - authentication.md: the example deploy role lists registration_info, like the role setup creates. - 5.4 release note: a rerun sets the variable again, and a policy another super user changes just before setup writes it is still overwritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say to pass workflow= when the workflow file isn't deploy.yaml Setup defaults to .github/workflows/deploy.yaml, create-harper's name, while the CI guide and the operations examples use deploy.yml. A missing default file is only reported as unverified, so setup could still pin deploy.yaml and leave a deploy.yml workflow's token unable to match. The reference now says so next to the command, with a workflow= example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Describe setup's create-only variable, the id-token check, and the checkout rule Follows HarperFast/harper#3127's review changes: setup leaves a HARPER_CLI_TARGET that already names another cluster, requires a job in the environment to grant `id-token: write`, and checks the local workflow only when the checkout is the repository the policy is for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say GITHUB_REPOSITORY, when set, names the checkout's repository Setup takes the checkout's repository from GITHUB_REPOSITORY before the origin remote, so the checkout rule now says both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say the setup race can only replace a policy created in the window Setup never writes a policy that already exists, so another super user's disable can't be overwritten; only a policy with the same id created in the moment before setup writes it can be (HarperFast/harper#3127). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`deploy:setup-ci` now also grants registration_info, the version check `harper deploy` makes before it uploads (HarperFast/harper#3127). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
## [1.13.0](v1.12.12...v1.13.0) (2026-10-14) ### Features * deploy on merge to main with GitHub OIDC instead of a stored refresh token ([20b31b5](20b31b5)), closes [#144](#144) [#145](#145) ### Bug Fixes * comment out a placeholder HARPER_CLI_TARGET in the scaffolded .env ([7ae2954](7ae2954)) * keep numeric directory names from reaching harper as numbers, and name a failed deploy ([9399a14](9399a14)) * name the component after the project directory, and order deploys with workflow concurrency ([95b9395](95b9395)) * refuse to re-run a deploy of a commit that is no longer the branch tip ([d1eff59](d1eff59)) * skip superseded deploys, and sanitize the project name before it reaches shell commands ([a82bc11](a82bc11)) * **studio:** drop a Deployment section that ends the README or has CRLF line endings ([0322a83](0322a83)) * **templates:** list @harperfast/schema-codegen in the vanilla templates' dependencies ([945344f](945344f)) * **templates:** write the rollback placeholder as DEPLOYMENT_ID, not <id> ([bbbfd6a](bbbfd6a)) ### Documentation * **templates:** list registration_info in the deploy role's operations ([4994f20](4994f20)), closes [HarperFast/harper#3127](HarperFast/harper#3127) ### Tests * read the deploy workflows with LF line endings, since a Windows checkout has CRLF ([6417bdc](6417bdc))
Regenerate the deploy rules against documentation@81c1b0e, which documents `harper deploy setup=true provider=github-actions` (HarperFast/harper#3127, HarperFast/documentation#722). deploying-from-ci now leads with that setup and the four conditions under which a job's harper commands authenticate, and keeps the hand-written role and policy for tag releases and customization, with `registration_info` added to the role. creating-harper-apps covers the deploy workflow create-harper 1.13.0 scaffolds (HarperFast/create-harper#147): deploy:setup-ci once, merge to main to deploy, npm run deploy by hand, and going back by deployment_id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerate the deploy rules against documentation@81c1b0e, which documents `harper deploy setup=true provider=github-actions` (HarperFast/harper#3127, HarperFast/documentation#722). deploying-from-ci now leads with that setup and the four conditions under which a job's harper commands authenticate, and keeps the hand-written role and policy for tag releases and customization, with `registration_info` added to the role. creating-harper-apps covers the deploy workflow create-harper 1.13.0 scaffolds (HarperFast/create-harper#147): deploy:setup-ci once, merge to main to deploy, npm run deploy by hand, and going back by deployment_id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
## [1.15.0](v1.14.3...v1.15.0) (2026-10-14) ### Features * **harper-best-practices:** deploy from CI with OIDC, and roll back by deployment_id ([b2f99fc](b2f99fc)), closes [HarperFast/documentation#711](HarperFast/documentation#711) * **harper-best-practices:** set up CI deploys with one command ([8b6092e](8b6092e)), closes [HarperFast/harper#3127](HarperFast/harper#3127) [HarperFast/documentation#722](HarperFast/documentation#722) [HarperFast/create-harper#147](HarperFast/create-harper#147) ### Bug Fixes * **harper-best-practices:** anchor deploying-from-ci on the pinned CLI install ([5490f3f](5490f3f)) * **harper-best-practices:** keep cluster credentials out of GitHub secrets ([6c39753](6c39753))
⊙ Problem
A GitHub Actions workflow can deploy with its OIDC identity token instead of a stored credential, but the cluster needs a deploy role, a user, and a trust policy whose claims match the workflow exactly. Writing those by hand is error-prone, and a mismatch shows up only as a 401 in CI.
❓ Your call: does this belong in core?
It is a provider on
harper deploy setup=true, at the maintainer's direction, instead of HarperFast/create-harper#145's script in every template. A "no" moves it back into create-harper as a script; the provider is additive, so removing it is easy.💡 Solution
harper deploy setup=true provider=github-actions project=my-app, run once from the repository's checkout:main, and theproductionenvironment;HARPER_CLI_TARGETvariable if it has none.It only creates. A rerun changes nothing, and a record or variable that already differs stops it before it writes.
⚖️ Alternatives
❓ Your call: is the policy-write race acceptable?
Setup never writes an existing policy, so it can't undo a disable or a drop. A policy with the same id that another super user creates in the one round trip before the write would be replaced. A no-replace flag on
add_oidc_trustwould narrow that, not close it, sincehdb_oidc_trustreplicates.❓ Your call: should a failed repository lookup refuse?
workflow_refis compared exactly, so setup looks the repository up for GitHub's spelling. When that fails (a private repo withoutgh), it proceeds with the name as typed and says so. Refusing would block that case entirely.🔧 Changes
bin/deploySetup.tsasks for the provider first and routesgithub-actionsbefore the secrets-custody fetch it doesn't need. Component resolution is shared.bin/deploySetupGithubActions.tsis the new provider:<project>-ci-deployandgithub-actions-<project>, which Studio's setup (Set up deploys from GitHub Actions: trust policy, deploy role and workflow studio#1788) must match; the role's operations aredeploy_component,get_jobandregistration_info, and setup says the role can deploy any component (Scope deploy permissions to component names #3046);1.0stays a branch name. It refuses a workflow with no job environment and one where no job in the environment grantsid-token: write, and runs only in the target repository's own checkout;structure_userorcluster_useris a conflict, and the claim-by-claim readback;ghcommand when it isn't set.bin/cliOperations.tsalso keeps each argument as typed on a non-enumerable_rawArgs, read byrawStringArg, sincebuildRequestJSON-parsesbranch=1.0into1. It exportsresolveGitRepo.security/DESIGN.mdrecords the create-only rule, the names Studio must match, and the race.✅ Verification
deploy:setup-ci, then its workflow's own deploy steps with no stored credential. The exchange authenticated, the release wascertified, andget_jobreturned COMPLETE.Every case run live, and the local gate results
acme/my-appto GitHub'sAcme/My-App; the scaffoldeddeploy.yamlpassed the check; a rerun changed nothing; noghexits 1 with the command; a widened role and an inactive user are refused and left as they were; a workflow on another branch, or withoutid-token: write, is refused before any write;branch=1.0is pinned as typed; credentials in the target URL authenticate every call; a differently spelled workflow path pins the canonical one.user:password@removed.acme/my-appwithrepo=acme/otherskips the workflow check with a note.issuerchanged, plus thesubthe generic profile requires). The same token was refuseddescribe_all. A token from another branch or environment, or afterdrop_oidc_trust, was refused, and the deploy failed.npm run lintandtest:unit:resources(4071) pass.test:unit:mainhangs atcomponents/applicationSpawn.test.jsand fails six files, exactly as a cleanorigin/mainbuild does here; CI's Ubuntu run is the gate for them.🤖 Generated with Claude Code
Release target: v5.4 — new CLI surface, not a backport; HarperFast/create-harper#147 needs it in 5.4.0
Related PRs: none found (partial scan: 3 PRs merged since 2026-10-08 beyond the first 50 were not scanned)
Complexity: medium
Review-Coverage: authored=claude; ran=gemini,cursor-muse,codex,cursor-composer; adjudicated=domain; blocked=cursor-grok(failed),claude(fallback)(out-of-budget); declined=cursor-kimi; rounds=8; full=3 @ abe0935
Review-Attention: read ~8m (decisions: do-less-alternative, variable-create-only, workflow-check-hard-refusal, checkout-identity-gate, client-composition-vs-server-create-only, exact-operation-set-match, cluster-wide-deploy-role) @ abe0935