Git Workflow » History » Version 6
Redmine Admin, 11/02/2026 02:18 AM
deploy-publish.sh
| 1 | 4 | Redmine Admin | *Chapter 3.1 of the dmz-web documentation · headings are numbered like the cheat sheets · index: [[Wiki]]* |
|---|---|---|---|
| 2 | |||
| 3 | {{>toc}} |
||
| 4 | |||
| 5 | 2 | Redmine Admin | Repos are bare, owned by the restricted host user `git` (git-shell) — the one |
| 6 | 1 | Redmine Admin | deliberate exception to the no-host-packages rule. Redmine only *reads* them. |
| 7 | |||
| 8 | **Who does what — this is the part that causes confusion, so read it first:** |
||
| 9 | |||
| 10 | 2 | Redmine Admin | | Step | Run as | Why | |
| 11 | |----------------------------------------|-----------------|------------------------------------------------------------| |
||
| 12 | | create bare repo, ACL, SELinux label | **root** | `/srv/git` is root/git territory | |
||
| 13 | | `git add` / `git commit` in a working copy | **owner user** (websvcmaster) | keeps `.git/objects` owned by that user | |
||
| 14 | | commit + push, day to day | **root**, via `deploy-commit.sh` | it commits *as websvcmaster* and pushes *as root* itself — the only command you normally type | |
||
| 15 | | `git push` by hand | **root** | push needs `sudo -u git git-receive-pack`; websvcmaster has **no sudo** and never will | |
||
| 16 | | register repo in Redmine | Redmine admin UI | | |
||
| 17 | 1 | Redmine Admin | |
| 18 | 2 | Redmine Admin | websvcmaster's password is **locked** on purpose (`passwd -l`, set by |
| 19 | `01-bootstrap-root.sh`). Enter it only via `su - websvcmaster` from root — never |
||
| 20 | set a password for it. If a command run as websvcmaster asks for a password, it |
||
| 21 | is a root command run in the wrong shell: `exit` to root and run it there. |
||
| 22 | 1 | Redmine Admin | (`passwd -S websvcmaster` must show `L`.) |
| 23 | 2 | Redmine Admin | |
| 24 | 1 | Redmine Admin | Repos: `mds-dark.git` (WordPress theme, project "MDS / MIMMAR Web Page Design"), |
| 25 | `dmz-web-deploy.git` (this host's `~/deploy`, project "dmz-web Infrastructure"). |
||
| 26 | |||
| 27 | 4 | Redmine Admin | ### 3.1.1 How Redmine reads them |
| 28 | 1 | Redmine Admin | |
| 29 | 2 | Redmine Admin | ```ini |
| 30 | 1 | Redmine Admin | # site-redmine.container |
| 31 | Volume=/srv/git:/srv/git:ro |
||
| 32 | Environment=GIT_CONFIG_COUNT=1 |
||
| 33 | Environment=GIT_CONFIG_KEY_0=safe.directory |
||
| 34 | Environment=GIT_CONFIG_VALUE_0=* |
||
| 35 | ``` |
||
| 36 | |||
| 37 | 2 | Redmine Admin | Rootless UID mapping: container uid *n* → host uid **200000 + n − 1**; Redmine |
| 38 | runs as uid 999 → host **200998** (`podman top site-redmine user huser`). |
||
| 39 | 1 | Redmine Admin | One-time host prep (root): |
| 40 | |||
| 41 | ```bash |
||
| 42 | semanage fcontext -a -t container_file_t '/srv/git(/.*)?' |
||
| 43 | restorecon -Rv /srv/git |
||
| 44 | ``` |
||
| 45 | |||
| 46 | 4 | Redmine Admin | ### 3.1.2 New bare repo — root |
| 47 | 1 | Redmine Admin | |
| 48 | 2 | Redmine Admin | ```bash |
| 49 | 1 | Redmine Admin | R=/srv/git/<repo>.git |
| 50 | sudo -u git git init --bare $R |
||
| 51 | sudo -u git git -C $R symbolic-ref HEAD refs/heads/main # bare HEAD defaults to master; every repo here uses main |
||
| 52 | setfacl -R -m u:200998:rX $R # Redmine read access |
||
| 53 | setfacl -R -d -m u:200998:rX $R # ...also for files future pushes create |
||
| 54 | restorecon -Rv $R # "not reset as customized by admin" on every file = already container_file_t = OK |
||
| 55 | git config --global --add safe.directory /home/<user>/<workdir> # lets root push from that working copy |
||
| 56 | ``` |
||
| 57 | |||
| 58 | 4 | Redmine Admin | ### 3.1.3 Working copy, first time — owner user (`su - <user>` from root) |
| 59 | 1 | Redmine Admin | |
| 60 | 2 | Redmine Admin | ```bash |
| 61 | 1 | Redmine Admin | git config --global user.name "<user>" # once per user, or commit refuses |
| 62 | git config --global user.email "sysadmin@mdsdefense.com" # matching a Redmine user's email links commits to them |
||
| 63 | cd <workdir> |
||
| 64 | git init -b main |
||
| 65 | # .gitignore first: no *.env, *secret*, *password*, *.key/*.pem/*.crt, dumps, archives, data/, tmp/ |
||
| 66 | git add -A && git status --short |
||
| 67 | grep -nEi 'password|secret|token|key_base' -r . --exclude-dir=.git | grep -viE 'Secret=|podman secret|\$\{?[A-Z_]*(PASSWORD|SECRET)' # must be comments/prompts only |
||
| 68 | git commit -m "initial import $(date +%F)" |
||
| 69 | git remote add origin /srv/git/<repo>.git |
||
| 70 | git log --oneline |
||
| 71 | exit # back to root |
||
| 72 | ``` |
||
| 73 | |||
| 74 | 4 | Redmine Admin | ### 3.1.4 Push — root |
| 75 | 1 | Redmine Admin | |
| 76 | First push of a new working copy (once): |
||
| 77 | |||
| 78 | 2 | Redmine Admin | ```bash |
| 79 | 1 | Redmine Admin | cd /home/<user>/<workdir> |
| 80 | git push --receive-pack='sudo -u git git-receive-pack' -u origin main |
||
| 81 | chown -R <user>:<user> .git # root's push leaves root-owned files in .git |
||
| 82 | ``` |
||
| 83 | |||
| 84 | Expected: `* [new branch] main -> main`. Verify: `sudo -u git git -C /srv/git/<repo>.git log --oneline`. |
||
| 85 | After that, never push by hand — use `deploy-commit.sh` (E). |
||
| 86 | |||
| 87 | 4 | Redmine Admin | ### 3.1.5 Redmine |
| 88 | 1 | Redmine Admin | |
| 89 | 2 | Redmine Admin | Project → Settings → Repositories → New repository: SCM **Git**, Main repository |
| 90 | ✓, Identifier `<repo>`, Path `/srv/git/<repo>.git`, Report last commit ✓ → Create. |
||
| 91 | Open the Repository tab; the commit must be listed. If not: |
||
| 92 | `podman exec site-redmine git -C /srv/git/<repo>.git log --oneline` — |
||
| 93 | 4 | Redmine Admin | "dubious ownership" → safe.directory env missing; "permission denied" → ACL (step 3.1.2). |
| 94 | 1 | Redmine Admin | |
| 95 | 4 | Redmine Admin | ### 3.1.6 Day-to-day — `deploy-commit.sh` (root) |
| 96 | 1 | Redmine Admin | |
| 97 | 2 | Redmine Admin | One root command does the whole cycle in the right identities: `git add -A` |
| 98 | and `git commit` **as websvcmaster** (identity auto-set), a guard that refuses |
||
| 99 | to commit staged lines that look like literal credentials, `git push` |
||
| 100 | **as root** via `sudo -u git git-receive-pack`, optional annotated tag, and |
||
| 101 | `chown` of `.git` back to websvcmaster. Source: `root-bin/deploy-commit.sh` in |
||
| 102 | 1 | Redmine Admin | the repo; installed as `/usr/local/sbin/deploy-commit.sh` (0750). |
| 103 | |||
| 104 | 2 | Redmine Admin | ```bash |
| 105 | 1 | Redmine Admin | deploy-commit.sh -n # dry run: shows what is staged, commits nothing |
| 106 | deploy-commit.sh -m "what changed" # commit + push |
||
| 107 | deploy-commit.sh -m "cheat sheet v1.1" -t cheatsheet-1.1 # ...+ tag |
||
| 108 | deploy-commit.sh -t cheatsheet-1.1 # nothing to commit: tag HEAD, push the tag |
||
| 109 | deploy-commit.sh -y -m "..." # skip the credential guard after checking the flagged lines |
||
| 110 | deploy-commit.sh -h |
||
| 111 | ``` |
||
| 112 | |||
| 113 | 2 | Redmine Admin | Editing still happens as websvcmaster (`su - websvcmaster`, edit, `exit`); |
| 114 | `deploy-commit.sh` never edits files, only `.git`. The guard flags staged lines of |
||
| 115 | the form `<…password|secret|token|api_key|app_key…> = <literal of 8+ chars>`; |
||
| 116 | 1 | Redmine Admin | variables (`$X`), placeholders (`<…>`, `...`), Quadlet `Secret=` lines and |
| 117 | 2 | Redmine Admin | comments are ignored. If it fires: move the value to a Podman secret or |
| 118 | `.gitignore` the file and re-run; if the lines are harmless, re-run with `-y`. Install/update the script: |
||
| 119 | 1 | Redmine Admin | |
| 120 | 2 | Redmine Admin | ```bash |
| 121 | install -o root -g root -m 0750 /home/websvcmaster/deploy/root-bin/deploy-commit.sh /usr/local/sbin/deploy-commit.sh |
||
| 122 | ``` |
||
| 123 | 1 | Redmine Admin | |
| 124 | 4 | Redmine Admin | ### 3.1.7 Publishing finished work — `deploy-publish.sh` (root) |
| 125 | 1 | Redmine Admin | |
| 126 | 2 | Redmine Admin | Replaces the manual "copy from `/root` into the repo, fix owner, commit, tag, type the wiki into |
| 127 | Redmine" cycle. A delivery is **one file**, `dmz-web-release-<version>.zip` (`<version>` = the cheat sheet |
||
| 128 | version of that release), dropped into `/root`; run `deploy-publish.sh` as root. It calls |
||
| 129 | `deploy-commit.sh` for the git part, so E still applies. |
||
| 130 | 1 | Redmine Admin | |
| 131 | 2 | Redmine Admin | The package holds the changed files (scripts, the cheat sheet), a `wiki/` folder (pages + png), |
| 132 | 6 | Redmine Admin | `RELEASE` (version, build date) and `SHA256SUMS`, and optionally a `releases/` folder with the notes of that release (what changed, steps, what to look at, rollback). What the script does: |
| 133 | 1 | Redmine Admin | |
| 134 | 2 | Redmine Admin | 0. **Package:** unpacked into a fresh private directory; paths are checked (no `..`, absolute paths |
| 135 | or symlinks), the file name must match `RELEASE`, `SHA256SUMS` must verify, and a file that is not |
||
| 136 | 6 | Redmine Admin | listed in `SHA256SUMS` rejects the package. Then the **sha256 of the package itself is printed** and compared: with `-s <sha256>` (the value from the delivery note) the run stops on a mismatch and, when it matches, says so and continues by itself; without `-s` it asks *continue?* (on a terminal) so you can compare by eye. That checksum is the only preflight: if the package carries a **newer `deploy-publish.sh`** than the running one, the script installs it to `/usr/local/sbin` and continues the same run with the new copy (a dry run uses the copy from the package and installs nothing). A manual install is therefore needed only for the step from 1.4 to 1.5. |
| 137 | 2 | Redmine Admin | 1. **Repo:** each known file is compared by its **version header** with the repo / installed copy: |
| 138 | newer → installed (repo files owned by websvcmaster, `/usr/local/sbin` copies root 0750); |
||
| 139 | identical → nothing to do; **older, same version with different content, or no version header → |
||
| 140 | refused** (stops a stale copy overwriting a newer one; `-f` overrides). Known files: cheat sheet, |
||
| 141 | `snipeit-install.sh`, `add-wordpress-site.sh`, `redmine-install.sh`, `check-sites.sh`, |
||
| 142 | `verify-host-deps.sh`, `deploy-commit.sh`, `deploy-publish.sh` — add a line to the `ITEMS` table in |
||
| 143 | 6 | Redmine Admin | the script for another one. `releases/*.md` of the package are copied to `releases/` in the repo: a new note is installed, an identical one is skipped, a note that exists with other content is **refused** (release notes are history; `-f` overrides). |
| 144 | 2. **Commit:** the sha256 of the package is appended to `releases/CHECKSUMS.txt` (a zip cannot hold its own hash); `deploy-commit.sh -m "release <version>: <files and versions>"`; a cheat sheet install also |
||
| 145 | 2 | Redmine Admin | gets the tag `cheatsheet-<version>` (skipped if that tag exists). |
| 146 | 3. **Wiki:** `wiki/*.md` → Redmine REST API: page created or updated (parent `Wiki`, the duplicate |
||
| 147 | `# title` first line dropped, `.png` attached once, unchanged pages skipped). `@@COMMIT@@` in a page |
||
| 148 | (the Releases table) becomes the commit made in step 2 (on a re-run: the commit of the tag `cheatsheet-<version>`; a page that needs a hash and has none is skipped, never overwritten with the placeholder). |
||
| 149 | 4. **Tidy:** after a complete, clean run the package is **moved** (never deleted) to |
||
| 150 | `/root/deploy-done/<timestamp>/`. With refusals, errors, `-n`, `-R` or `-W` it stays in `/root`; running it |
||
| 151 | again is safe, handled files then show as *identical* and are skipped. |
||
| 152 | 1 | Redmine Admin | |
| 153 | 2 | Redmine Admin | Loose files (no package) still work: put them into `/root` (and `dmz-web-wiki-pages.zip` for the wiki) |
| 154 | and the handled ones are moved to `/root/deploy-done/<timestamp>/`. |
||
| 155 | |||
| 156 | 1 | Redmine Admin | ```bash |
| 157 | 2 | Redmine Admin | deploy-publish.sh -n # dry run: the plan, wiki read-only GETs, nothing changed or moved |
| 158 | deploy-publish.sh # do everything with the newest /root/dmz-web-release-*.zip |
||
| 159 | deploy-publish.sh -R # wiki only deploy-publish.sh -W # repo only |
||
| 160 | 6 | Redmine Admin | deploy-publish.sh -s <sha256> # stop unless the package has this sha256 (from the delivery note), else carry on |
| 161 | 2 | Redmine Admin | deploy-publish.sh -m "msg" # own commit message deploy-publish.sh -y # skip deploy-commit's guard |
| 162 | deploy-publish.sh -f # install even when older / equal / no version header |
||
| 163 | 5 | Redmine Admin | deploy-publish.sh --status # read-only overview: published / installed / waiting (see below) |
| 164 | 1 | Redmine Admin | ``` |
| 165 | 5 | Redmine Admin | |
| 166 | **What is published, installed and waiting?** `deploy-publish.sh --status` answers it and changes nothing: the version of every file in the repo and in `/usr/local/sbin` (a difference is flagged), HEAD and the last `cheatsheet-*` tag, uncommitted or unpushed commits, release packages and `pf-app-*.tar.gz` waiting in `/root` (flagged *NOT PUBLISHED YET* when newer than the last tag), and for PF the `Image=` tag in the repo unit, in the installed unit and of the running `site-pf` container. Lines starting with `!!` are the things to look at. |
||
| 167 | 1 | Redmine Admin | |
| 168 | 2 | Redmine Admin | One-time setup for the wiki step: `deploy-publish.sh --init` writes `/root/.deploy-publish.conf` |
| 169 | (mode 0600; set `RM_PROJECT` to the project **identifier**, Project settings → Identifier); put the |
||
| 170 | Redmine API key (My account → API access key) into `/root/.redmine-api-key`, `chmod 600`; Redmine |
||
| 171 | Administration → Settings → API → *Enable REST web service*. Requests go to NPM on this host |
||
| 172 | (`--resolve track.mdsdefense.com:444:127.0.0.1`), like `check-sites.sh`. The page text in Redmine is |
||
| 173 | overwritten by the package's text — the first real run replaces pages that were typed in by hand, so |
||
| 174 | 1 | Redmine Admin | start with `-n`. Bootstrap the first time (the script cannot install itself before it exists): |
| 175 | |||
| 176 | 2 | Redmine Admin | ```bash |
| 177 | install -o root -g root -m 0750 /root/deploy-publish.sh /usr/local/sbin/deploy-publish.sh |
||
| 178 | deploy-publish.sh --init && deploy-publish.sh -n |
||
| 179 | ``` |
||
| 180 | |||
| 181 | Afterwards the script updates itself from the package (the running copy is replaced safely). |
||
| 182 | |||
| 183 | 4 | Redmine Admin | ### 3.1.8 Recovering from the usual mistakes |
| 184 | 1 | Redmine Admin | |
| 185 | * `fatal: could not open '.git/COMMIT_EDITMSG': Permission denied` (or any |
||
| 186 | 2 | Redmine Admin | permission error on commit) → root touched `.git` (committed there, or pushed/tagged |
| 187 | without `deploy-commit.sh`). Fix: `chown -R <user>:<user> /home/<user>/<workdir>/.git`, |
||
| 188 | then use `deploy-commit.sh` from now on (it chowns every time). Also check root's `~/.gitconfig` didn't pick up the user's identity. |
||
| 189 | * `Everything up-to-date` on push right after you expected a new commit → the |
||
| 190 | commit didn't happen (identity error or permission error scrolled past); `git log --oneline` |
||
| 191 | and `git status --short` in the working copy. A tag made at that moment points at the |
||
| 192 | old commit: `deploy-commit.sh -t <tag>` after the real commit (it force-moves the tag). |
||
| 193 | 4 | Redmine Admin | * `error: src refspec main does not match any` → no commit yet on `main` (step 3.1.3 not done). |
| 194 | 2 | Redmine Admin | * `error: key does not contain a section: receivepack` → wrong syntax; use |
| 195 | 4 | Redmine Admin | `--receive-pack=` (step 3.1.4), not `-c receivepack=`. |
| 196 | 2 | Redmine Admin | * `sudo: a password is required` / password prompt → you are websvcmaster; this is a root step (`exit`, then `deploy-commit.sh`). |
| 197 | 4 | Redmine Admin | * Redmine shows the repo empty → bare HEAD still points to `master` (step 3.1.2 `symbolic-ref`). |
| 198 | 2 | Redmine Admin | * `deploy-publish.sh`: `REFUSED: no version header` → the copy is an old/unversioned one; fetch the current file or `-f` if you are sure. `checksum mismatch` / `does not match RELEASE` → the package is damaged or was renamed; get it again. `HTTP 401/403` on the wiki step → API key wrong, REST not enabled, or the account may not edit this project's wiki. `HTTP 404` for every page → wrong `RM_PROJECT` (it must be the identifier, not the project name). |
| 199 | 1 | Redmine Admin | |
| 200 | --- |
||
| 201 | 3 | Redmine Admin | _Source: `dmz-web-podman-cheatsheet.md` v1.5 in the `dmz-web-deploy` repo ([[Releases]]). Edit the wiki and the repo copy together._ |