what setup_autodeploy.py has to get right when it writes to a repository
that isn't this one.
there is no "set this secret to this string" call. github publishes a public key per repository and accepts only values sealed against it:
GET /repos/{owner}/{repo}/actions/secrets/public-key -> {key, key_id}
PUT /repos/{owner}/{repo}/actions/secrets/{name} <- {encrypted_value, key_id}
encrypted_value is a libsodium sealed box (anonymous, no sender key),
base64-encoded β which is why pynacl is a dependency for what otherwise looks
like a pure-http script.
the failure mode is quiet: a value encrypted wrong is still valid base64, so
github accepts it with a 201. nothing goes wrong until the deploy workflow
runs and the ftp server rejects the password, several minutes and one repo
away from the actual mistake. the tests round-trip a real keypair for exactly
this reason.
201 means created, 204 means an existing secret was overwritten. both are
success, and the script says which happened β overwriting a secret somebody set
by hand is worth seeing in the log.
you cannot create a repository secret called GITHUB_PAT. github rejects the
whole GITHUB_ prefix. the workflows here store it as DEPLOY_GITHUB_PAT and
map it onto the env var the script reads.
the env var itself is deliberately not called GITHUB_TOKEN: that name is
always populated inside actions, with a token scoped to the repo running the
job. reusing it would mean silently picking up a token that cannot write to the
target repo, and getting a 404 that looks like a typo.
github returns 404 for a private repository the token can't see, rather than
403, so it doesn't leak the existence of private repos. so a 404 here means
either a wrong name or a token without access, and the message says so β
guessing wrong costs a round of re-checking the spelling of something spelled
correctly.
PUT /repos/{owner}/{repo}/contents/.github/workflows/<file> is refused with a
403 unless the token carries workflow (classic pat) or Workflows: write
(fine-grained) β even when it can already write every other path in the repo.
the message mentions "workflow", which is what the script matches on to explain
the fix rather than printing a bare 403.
the file says "run on push to master". the commit that creates it is a push to
master. so the setup run's last action publishes the repo β intentional as a
smoke test, but it means the operation isn't dry at the end.
it's also the reason the ftp account and the secrets are created first: by the time the workflow exists, everything it needs already does.
hammerspace's own workflows declare permissions: {} β they talk to cpanel and
github with their own credentials and want nothing from the automatic
GITHUB_TOKEN. copying that line into the generated deploy workflow was
wrong: actions/checkout reads the repository through that token, and on a
private repo with no permissions it fails as:
remote: Repository not found.
fatal: repository 'https://github.com/owner/repo/' not found
which reads like a typo or a deleted repo, not a permissions problem. it also would not have shown up on a public repo, where the anonymous clone succeeds.
the generated workflow declares contents: read and nothing else. found on the
first real deploy.
the contents api takes the existing blob sha and overwrites. same shape as
Fileman::save_file_content on the cpanel side, and the same handling: read
first, no-op if identical, refuse if it differs. a deploy workflow already in
the repo may well have been edited by hand.
the one case a read can't catch: the doc's manual procedure names the file
deployTocPanel.yml, this one writes deploy-to-cpanel.yml. different paths,
so nothing is overwritten and both run β two deploys per push, racing over the
same ftp directory. the preflight looks for the old name specifically and stops.
a repo created on github and never pushed to answers differently depending on which endpoint you ask. checked against a real one:
| call | answer |
|---|---|
GET /repos/{owner}/{repo} |
200, with a default_branch that doesn't exist yet |
GET /repos/{owner}/{repo}/branches |
200, [] |
GET /repos/{owner}/{repo}/branches/master |
404 "Branch not found" |
GET .../commits, .../git/refs, .../git/trees/... |
409 "Git Repository is empty." |
so a missing branch looks the same on an empty repo as on a typo β which is
exactly how the first auto-deploy against a fresh repo failed.
setup_gitflow.py takes either an empty branch list or a 409 to mean "empty".
the git database api can't bootstrap it either: a ref needs a commit, and that
api refuses to create one on an empty repo. the way in is the contents api β
PUT /contents/README.md creates the first commit and its branch in one call.
after that everything behaves normally, which is why develop is created with
POST /git/refs right after (with a couple of retries, since github documents
the same 409 for a repo that isn't "available" yet).
the contents api's branch parameter is only documented as defaulting to the
default branch; how it behaves on a repo with no branches at all isn't. so it's
left out whenever the production branch is the default β the usual case β and
when it isn't, the script checks the branch really got created.