Skip to content

build: adopt Hephaestus and modernize development workflows - #1

Merged
magicdrive merged 6 commits into
mainfrom
build/adopt-hephaestus
Oct 25, 2026
Merged

magicdrive merged 6 commits into
mainfrom
build/adopt-hephaestus

Conversation

@magicdrive

@magicdrive magicdrive commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adopt Hephaestus v1.2.0-rc.1 for releases and add the CI, Renovate, Makefile targets and release docs this repository did not have. The new behavior tests found several existing bugs in git-parallel; the small, safe fixes are included (each covered by a test). The fixes that would change behavior are only listed in Remaining Risks.

Hephaestus v1.2.0-rc.1 and make publish

  • Pins: every uses: magicdrive/hephaestus@... is bae0e2b3cbf41ad27299e0cfc3517a6affc7e15e # v1.2.0-rc.1 (the RC's tag commit) and HEPHAESTUS_VERSION ?= v1.2.0-rc.1. The v1.1.0 pin (550d48f) is gone.
  • No Go from the action: v1.2.0's action downloads its release binary and verifies the SHA-256 and the provenance attestation (bound to the pinned commit); it sets up no Go and leaves PATH/GOTOOLCHAIN alone. git-parallel builds nothing, so the release workflow and the hephaestus CI job need no Go. The jobs that run Go tools keep their own setup-go: lint (make actionlint) and homebrew-e2e (go install of the hephaestus CLI).
  • Release plan: the release job runs the action with command: plan (id: plan). The tap token check, Rehearse the formula and tap pull request (read-only) and Generate the formula and open a tap pull request run only if: steps.plan.outputs.tap == 'publish', so a prerelease never receives the tap token. CI plans v0.0.0-rc.1 and v0.0.0 and requires true/skip and false/publish.
  • make publish vX.Y.Z (new; also VERSION=vX.Y.Z and tag=vX.Y.Z, YES=1 skips the prompt), make publish-check|publish-status|publish-watch vX.Y.Z, make publish-help. They call $(PUBLISH) <publish|check|status|watch|help> -from-make with PUBLISH ?= $(HEPHAESTUS) publish; the version reaches the tool through HEPHAESTUS_PUBLISH_ARGS/VERSION/TAG, never a shell command line (the Makefile section is the one in Hephaestus docs/publish.md).
  • Embedded version: make publish and make publish-check first run scripts/check-version.sh --from-make, which reads the same environment and requires git-parallel --version to be exactly the tag. A tag that the release workflow's build job would refuse is never pushed. publish-check reports both the version check and the preflight and fails if either fails; an invalid or missing version stops before the tool runs. check-version.sh now accepts only SemVer 2.0.0 tags with a leading v (as hephaestus publish does) and exits 2 for usage errors.
  • Behavior change: releasing is no longer git tag + git push by hand: make publish checks, confirms, pushes one annotated tag, follows the release run and verifies the release (assets, attestations, notes, tap PR). There was no previous publish target. version-check, preflight and release-check are unchanged.
  • Tests: tests/publish-make.sh (make test-publish, part of make check) drives the targets with a stub tool: one call with the right arguments and environment, -j4, make -n; no call for two versions, a malformed version, another target in the same command, publish-help with a version, shell metacharacters, or a version the script does not report. CI runs it in every test job (GNU Make 4 on Linux, 3.81 on macOS).

Before / After

Before After
CI none ci.yml: ShellCheck, actionlint, Renovate validation, behavior tests (bash 5 on Linux amd64/arm64 and macOS; bash 3.2 on macOS), Hephaestus check + snapshot, Homebrew E2E (Linux, macOS), ci-gate
Release manual tag + manual GitHub Release, no assets make publish vX.Y.Z pushes the tag after its checks; release.yml on v* tags: full CI → read-only build job → release environment job (plan, tap dry-run, attestation, GitHub Release, tap PR)
Homebrew no formula new git-parallel formula, opened as a PR on magicdrive/homebrew-tap by the first release
Dependencies — renovate.json (actions pinned by digest, Makefile tool pins through customManagers)
macOS /bin/bash 3.2 broken: exec: {FD}: not found, exit 127 works (tested in CI)

Hephaestus Integration

  • hephaestus.yaml: no builds. One noarch archive (targets: [{os: all, arch: all}]), named git-parallel_<version>_noarch.tar.gz, with git-parallel (0755), git-parallel.1 (0644) and README.md (0644) at its root. Checksum file git-parallel_<version>_checksums.txt.
  • release.github: magicdrive/git-parallel, generate_notes: true (there is no changelog).
  • brews: git-parallel, ids: [script], tap root (no directory), pull_request.enabled: true, skip_upload: auto, the standard commit_msg_template. install: bin.install "git-parallel", man1.install "git-parallel.1". test: --version (expects exit 1, see Remaining Risks), --list and -- status in a new git repository.
  • No depends_on: the script now runs on the system bash 3.2 and on later versions, and git is a precondition of a git subcommand.
  • preflight: workflow: release.yml, environment: release.
  • Stages are archive,checksum (not build,archive,checksum): with no builds configured, hephaestus run --stages build fails with "no builds are configured".
  • The version is written into the script (git::parallel::version) and is not generated at release time. scripts/check-version.sh (in the release build job and in make preflight) refuses a tag that does not equal git-parallel --version.

CI

.github/workflows/ci.yml (push: main, pull_request, workflow_dispatch, workflow_call; permissions: contents: read; per-ref concurrency that never cancels a run called from release.yml; timeout-minutes on every job):

  • lint: make lint (ShellCheck on all scripts), make actionlint, make renovate-check (--strict)
  • test matrix: ubuntu-latest bash 5, ubuntu-24.04-arm bash 5, macos-latest /bin/bash 3.2, macos-latest Homebrew bash 5. The step checks the bash major version, so a missing bash cannot pass silently. Then make test and a --version/--help smoke test.
  • test also runs make test-publish.
  • hephaestus: the action's check, release plans for an RC and a stable version, then a snapshot archive,checksum with github-token: "". It verifies the archive listing, the modes, the checksum, and that the archived script matches the checkout.
  • homebrew-e2e (ubuntu-latest, macos-latest): make homebrew-e2e
  • ci-gate: passes only if every job succeeded

Release workflow

.github/workflows/release.yml follows examples/workflows/release-split-jobs.yml and Hephaestus' own release workflow:

  • ci: uses: ./.github/workflows/ci.yml on the tagged commit
  • build: contents: read, no environment, no secrets. Runs the version check, then the action with stages: archive,checksum, fresh: "true", github-token: "". Verifies dist/ and uploads it.
  • release: needs: [ci, build], environment: release, contents: write, id-token: write, attestations: write. Steps, in order:
    1. command: plan (id: plan).
    2. Download dist/.
    3. Check that the tap token is set (only a boolean reaches the step); only when tap == 'publish'.
    4. Rehearse the formula and tap pull request (read-only); only when tap == 'publish'.
    5. attest-build-provenance (archive and checksum file).
    6. command: release.
    7. Generate the formula and open a tap pull request with github-token: "" and the tap token; only when tap == 'publish'.
  • Tokens reach Hephaestus only through action inputs. Every uses: is pinned to a full SHA. PERSONAL_GITHUB_TOKEN is not used.

Renovate

renovate.json (same baseline as Hephaestus):

  • github-actions with pinDigests: true and no automerge. This includes magicdrive/hephaestus@<sha> # v1.2.0-rc.1.
  • A "Hephaestus" group keeps the action and the Makefile CLI pin together (a Renovate lookup puts both in branch renovate/hephaestus; from a stable pin it does not propose release candidates).
  • A regex customManagers entry tracks the Makefile pins: HEPHAESTUS_VERSION (go), ACTIONLINT_VERSION (go) and RENOVATE_VERSION (npm).

renovate-config-validator --strict passes locally and in CI.

Makefile

all, install, uninstall, PREFIX and CURRENTDIR are unchanged, and make alone still prints the same message. make install/uninstall PREFIX=<tmp> were checked locally. New targets:

  • help, lint, test (TEST_BASH=...), check
  • actionlint, renovate-check
  • hephaestus-check, snapshot
  • version-check tag=, preflight tag= (version-check + hephaestus preflight --strict), release-check tag=
  • homebrew-e2e
  • test-publish; publish, publish-check, publish-status, publish-watch, publish-help (above)

.PHONY is declared for every target, and the targets work with GNU make 3.81.

Release Compatibility

v1.0.2 (the latest release) has no assets and the tap has no git-parallel formula, so nothing existing has to be matched and no current Homebrew user is affected.

Item v1.0.2 This PR
Assets none (source archives only) git-parallel_X.Y.Z_noarch.tar.gz, git-parallel_X.Y.Z_checksums.txt
Archive layout — README.md, git-parallel (0755), git-parallel.1 (0644), no top-level directory, reproducible (mtime 1980-01-01, uid/gid 0)
Checksum — sha256, sha256sum -c format
Version hard-coded git-parallel v1.0.2 unchanged mechanism; the release refuses a mismatching tag
Homebrew none new formula git-parallel (desc, homepage, MIT, one url, bin + man1, test)
make install PREFIX=~/local unchanged

Homebrew Verification

Run with scripts/homebrew-e2e.sh locally in homebrew/brew:latest (linux/amd64, Homebrew 4.6.20) and in CI on ubuntu-latest and macos-latest:

  1. Hephaestus archive,checksum,formula for the script's version.
  2. The archive layout and checksum are checked.
  3. The formula's url is pointed at the local archive (file://, same file name and sha256).
  4. In a throw-away tap: brew style, brew audit --strict, brew install, brew test.
  5. The installed git-parallel --version, git parallel -V, a two-repository run and the man page are checked.
  6. brew uninstall.

Nothing is written to magicdrive/homebrew-tap.

Security

  • All workflows: top-level permissions: contents: read. Write permissions only on the release job.
  • No pull_request_target. persist-credentials: false on every checkout.
  • The build job holds no secret and no write token.
  • The tap token is used only through action inputs, in the environment-gated job.
  • The build provenance attestation covers the archive and the checksum file.
  • The release stage never overwrites assets that differ (Hephaestus).
  • Tests check that command text is passed as words and never evaluated by a shell.

Tests

tests/run.sh: 20 test groups. The suite is bash 3.2 compatible and builds temporary trees of git repositories. It covers:

  • Options: --version/-V, --help/-h, unknown options and invalid values, --list, --maxdepth, -d/--target/--targets, --silent/--verbose/--color, --freeze.
  • Commands: git commands (with and without a leading git), ::: bare commands, git parallel as a git subcommand.
  • Output and quoting: output order, paths with spaces, argument quoting.
  • Failures and parallelism: error propagation and exit status, real parallelism with --jobs (a barrier), more repositories than jobs, --jobs 0.
  • Cleanup: temporary-file cleanup, and TERM stopping the running commands.

Against the original script, the suite fails on:

  • bash 5: paths with spaces, --targets, and --jobs 0 (hangs).
  • bash 3.2: every execution test (exit 127).

Fixes in git-parallel

  • bash 3.2 (macOS): exec {FD}< <(...) needs bash 4.1, so every run failed on /bin/bash. Job output now goes to per-job temporary files, printed in the same order once the batch finishes (same output as before).
  • Paths with spaces were split into words and skipped silently. Discovery now reads one path per line. It also no longer needs perl and keeps the same ./ and /.git stripping.
  • --targets, as the help and README spell it, was rejected as an unknown option. Both --targets and --target are accepted now.
  • --jobs 0 (or a non-numeric GIT_PARALLEL_DEAULT_JOBS) looped forever. It is now rejected with exit 1.
  • Interrupts on macOS: ps --ppid is GNU-only. pgrep -P is used now, and the temporary files are removed.
  • ShellCheck-clean. The intentional word splitting of the command string is annotated.

Remaining Risks

Kept for compatibility, documented by tests:

  • Exit status 0 when the command fails in some or all repositories.
  • --version, --help and --list exit 1.
  • The command is joined into one string and split again, so quoting inside an argument is not preserved (-- commit -m "a b").

Changing any of these would break scripts that rely on the current behavior. They are better done as a separate, explicit change.

Other risks:

  • The version must be bumped by hand before tagging. make publish refuses to tag if it is not, and the release build job refuses such a tag.
  • Hephaestus is pinned to a release candidate (v1.2.0-rc.1); move to v1.2.0 when it is released.
  • homebrew-e2e depends on the runners' Homebrew.
  • No LICENSE file. The MIT text lives in the script and README, and the formula declares license "MIT".

Release Migration Notes (owner, before the first release)

make publish-check v1.0.3 on this branch fails these owner items (besides the ones that this unmerged branch and the unbumped version cause):

  1. environment: the release environment's deployment policy allows only branch v*. A tag-push deployment will be rejected. Add a tag policy v*, and remove the branch v* policy: a branch named like a version could read the tap secret.
  2. main ruleset: add an active ruleset on main that requires the status check ci-gate.
  3. tag ruleset: add an active tag ruleset on refs/tags/v* that forbids deletion and update (not creation).

Then:

  1. Set git::parallel::version to the new version (e.g. v1.0.3) in a PR and merge it after CI.
  2. On main: make publish-check vX.Y.Z, then make publish vX.Y.Z.
  3. Merge the tap PR (it adds git-parallel.rb).

See docs/development/releasing.md. Rollback: revert this PR (no existing release or formula depends on it).

https://claude.ai/code/session_01RbGbwRHvCEhsKLZfbvbbRg

Release git-parallel with Hephaestus v1.1.0 as one noarch archive
(git-parallel, git-parallel.1, README.md), a checksum file, provenance
attestations and a Homebrew tap pull request for a new git-parallel
formula. Add CI (ShellCheck, actionlint, Renovate validation, behavior
tests on bash 3.2 and bash 5 on Linux and macOS, Hephaestus snapshot,
Homebrew E2E), a tag-triggered release workflow split into read-only
build and environment-gated publish jobs, Renovate, Makefile targets and
release documentation. Existing install/uninstall targets are unchanged.

Fix git-parallel bugs found by the new tests:
- run on the bash 3.2 that ships with macOS (named file descriptors,
  `exec {FD}<`, need bash 4.1; job output now goes to temporary files)
- handle repository paths containing spaces
- accept --targets as documented in the help (--target still works)
- reject --jobs 0, which looped forever
- stop child processes on HUP/INT/QUIT/TERM on macOS too (pgrep -P
  instead of GNU-only `ps --ppid`) and remove temporary files
- drop the perl dependency from repository discovery

Claude-Session: https://claude.ai/code/session_01RbGbwRHvCEhsKLZfbvbbRg
Pins the Hephaestus action and CLI to 4414722 (magicdrive/hephaestus#22,
pseudo-version v1.1.1-0.20261025105032-4414722a804d): the action no longer
puts its own Go on PATH or exports GOTOOLCHAIN=local, and redaction no
longer hides paths under PWD. This commit only verifies the fix in this
repository's CI; it is reverted by the next commit, which returns to the
released v1.1.0. Pin v1.1.1 once it is released.

Claude-Session: https://claude.ai/code/session_01RbGbwRHvCEhsKLZfbvbbRg
This reverts commit c83547e. Its CI run verified the Hephaestus
action and CLI of magicdrive/hephaestus#22 in this repository; the pull
request goes back to the released v1.1.0, so that it never asks to merge
an unreleased pin. Pin v1.1.1 (its tag's commit) once it is released.

Claude-Session: https://claude.ai/code/session_01RbGbwRHvCEhsKLZfbvbbRg
@magicdrive

Copy link
Copy Markdown
Owner Author

Hephaestus v1.1.1 fix verified in this repository (not yet released)

magicdrive/hephaestus#22 stops the Action from putting its own Go 1.26.0 on PATH and from exporting GOTOOLCHAIN=local. It also stops redaction from hiding paths under PWD.

  • c83547e pinned the Action and the CLI to that PR's commit 4414722 (pseudo-version v1.1.1-0.20261025105032-4414722a804d). CI passed: https://github.com/magicdrive/git-parallel/actions/runs/40187418112. The logs show that the Action no longer sets up its own Go, and that Hephaestus itself is built with go1.27.3.
  • 8ef8f04 reverts that commit, so this PR again pins the released v1.1.0, and CI is green again. The toolchain pins and the go version -m assertions stay as they are.

After v1.1.1 is released, update the pin to the commit of the v1.1.1 tag, together with HEPHAESTUS_VERSION in the Makefile. Until then, nothing in this PR refers to an unreleased version.

https://claude.ai/code/session_01RbGbwRHvCEhsKLZfbvbbRg

Pin every magicdrive/hephaestus action to the v1.2.0-rc.1 tag commit
(bae0e2b3cbf41ad27299e0cfc3517a6affc7e15e) and HEPHAESTUS_VERSION to
v1.2.0-rc.1. From v1.2.0 the action downloads and verifies its release
binary and sets up no Go: the release workflow and the hephaestus CI job
need none, and the jobs that run Go tools (actionlint, go install for the
Homebrew end-to-end test) keep their own setup-go.

Follow the hephaestus publish conventions in release.yml: a plan step
(id: plan, command: plan) and the tap token check, the read-only tap
rehearsal and the tap pull request step run only when
steps.plan.outputs.tap == 'publish', so a prerelease never receives the
tap token. CI plans a release candidate and a stable release and checks
that only the stable one publishes the tap.

Add make publish / publish-check / publish-status / publish-watch /
publish-help, wired as in Hephaestus's own Makefile: the version reaches
the tool through HEPHAESTUS_PUBLISH_* environment variables, never a shell
command line. publish and publish-check first run
scripts/check-version.sh --from-make, so a tag whose version git-parallel
does not report (and the release workflow would refuse) is never pushed.
tests/publish-make.sh checks the wiring with a stub tool and runs in CI
on GNU Make 4 (Linux) and 3.81 (macOS).

Claude-Session: https://claude.ai/code/session_01RbGbwRHvCEhsKLZfbvbbRg
Moves the action and HEPHAESTUS_VERSION from the release candidate
v1.2.0-rc.1 (bae0e2b, verified by this pull request's CI) to the stable
v1.2.0: magicdrive/hephaestus@160990d,
the commit the v1.2.0 tag points to. The release binary, its checksum and
its attestation are verified by the action for that commit.

Claude-Session: https://claude.ai/code/session_01RbGbwRHvCEhsKLZfbvbbRg
@magicdrive
magicdrive merged commit c25e715 into main Oct 25, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant