Repository navigation
fix(ci): replace the scaffold release workflow with a working one - #79
Conversation
The release workflow was the unfilled template: the build step echoed "Build your artifacts here", the SLSA provenance job attested an empty subject list, and git-cliff was fetched from releases/latest by a URL that no longer resolves. So tag v0.1.1 has no GitHub Release. The build job now tests the package on Julia 1.10 against General and the hyperpolymath registry (where Causals, ZeroProb, Cliodynamics and AcceleratorGate are registered), resolving fresh from [compat] with any committed manifest removed. It packages the source and a CycloneDX SBOM. The changelog uses orhun/git-cliff-action v4.9.1 with git-cliff pinned to v2.14.2. Provenance comes from actions/attest-build-provenance v4.2.2 on the release files, replacing the SLSA generator, whose nested actions cannot be pinned by SHA. A manual run takes an existing tag and checks it out, so the artefacts match the tagged tree. The version is passed through env, must match vX.Y.Z or vX.Y.Z-pre, and must already exist as a tag. A pull_request trigger on this file runs the build and changelog jobs at review; the release job is skipped there. The unused full-CHANGELOG.md artifact is dropped: nothing consumed it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XJoh8YA2sCWQmq93CF8RX
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (1)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📜 Recent review details⏰ Context from checks skipped due to timeout. (16)
|
| Layer / File(s) | Summary |
|---|---|
Build and package release artefacts .github/workflows/release.yml |
The workflow adds pull-request and manual triggers. It checks out the selected ref, builds and tests the Julia package, then uploads a source archive with a CycloneDX SBOM. |
Generate the changelog .github/workflows/release.yml |
The changelog job checks out the selected version with full history and uses git-cliff to generate the changelog. |
Validate and publish the release .github/workflows/release.yml |
The release job validates the tag, downloads the build artefacts, creates SHA-256 checksums and provenance attestations, then publishes the release files and changelog. |
Priority: ⬇️ Low
Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Bug fix
Sequence Diagram(s)
sequenceDiagram
participant GitHubActions
participant BuildJob
participant ChangelogJob
participant ReleaseJob
participant GitHubRelease
GitHubActions->>BuildJob: Start package build
GitHubActions->>ChangelogJob: Generate changelog for selected ref
BuildJob->>ReleaseJob: Upload source archive and SBOM
ChangelogJob->>ReleaseJob: Provide generated changelog
ReleaseJob->>GitHubRelease: Publish validated version with release files
Merge Risk
Merge Risk: ⚪ Minimal · up to cc337
The release workflow now builds, tests, packages, attests and publishes releases with stricter tag validation. No concrete merge-blocking issue was identified; the release job will first run after merge, so check the first manual release.
-
Security Architecture Review
Security architecture risk: 🔵 Low · up to
cc337The workflow substantially improves release validation and separates package execution from publishing credentials. A remaining integrity concern is that manual releases select a mutable tag without binding all stages to one commit. Its exposure depends on who can change release tags. Repository protection settings and interrupted-publication behavior were not available for verification.
Retained concerns
- Medium · security · inferred: Manual release stages are not bound to one immutable source commit. Build and changelog independently checkout inputs.version, while publication validates only that the named tag exists. If an authorized actor moves that tag between stages, the workflow can publish artifacts and release notes from different revisions, or attach artifacts to a tag that now identifies another revision. Checksums and artifact attestation do not provide an explicit workflow-level comparison with the tag's commit. The new manual path and publication of actual package files introduce this integrity exposure beyond the base scaffold; external tag protections could prevent the triggering condition.
-
Security review details
Security Blast Radius
- inferred — The visible sensitive outcomes are this repository's release assets and attestations, with downstream exposure to consumers trusting those package files. Package and dependency code executes in the read-only build job but can influence the workspace subsequently packaged. No cloud role assumption or application data-store access is configured in the inspected workflow; external OIDC trust policies were not available.
Security Findings and Attack Paths
- inferred — The conditional integrity path requires authority to move the selected release tag: a manual build can checkout revision A, the tag can move to revision B before another stage, and the existence check can still pass. Publication can then associate artifacts from A with the tag now naming B. This is not a demonstrated unprivileged exploit, and effective tag-protection policy could eliminate the condition.
Trust Boundaries and Controls
- observed — Job separation keeps package build/test execution outside the job holding publishing credentials. Pull-request publication is excluded, manual input is passed through an environment variable and shell-quoted, version syntax and remote existence are checked, and actions are SHA-pinned. These controls constrain credential exposure and malformed inputs, but do not explicitly bind a selected tag to a recorded commit throughout the release.
Resilience and Maintainability Implications
- inferred — The same-run artifact handoff preserves build-to-publication ordering, but fresh dependency resolution means repeated builds of one tag can produce different SBOM contents. Without configured same-tag serialization, concurrent runs can independently reach publication. Whether they replace assets, leave a partial release, or recover idempotently remains an external-action coverage gap rather than an established failure.
Hardening Proposals
- proposed — Resolve the fully qualified release tag once to an immutable commit, use that commit for both checkouts, and verify its relationship to the publication tag before publishing. For recovery, consider serializing runs by release tag and defining whether retries reuse an existing artifact set or intentionally replace it.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
✨ Finishing Touches
🛠️ Fix failing CI checks
- Commit to this branch
- Create a new PR
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
A rabbit checks the tag at dawn,
Then watches Julia build along.
A source bundle and SBOM hop,
While changelog lines fall into place.
Checksums join the release parade,
And provenance trails the files.
Comment @coderabbitai help to get the list of available commands.
actions/checkout resolves an unqualified ref to a branch of that name before a tag (src/ref-helper.ts:52-57 at the pinned SHA). If a branch named like the tag existed, a manual run would package that branch, while the release job only confirms that the tag exists. Both checkouts now pass refs/tags/<version> on a manual run, and an empty ref (the triggering ref) on push and pull_request runs, as before. Raised by CodeRabbit on hyperpolymath/Causals.jl#69. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XJoh8YA2sCWQmq93CF8RX
|
The CI failures may require manual intervention. View agent analysis |
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
-->
## Summary
The release workflow has failed on every tag. Run 37866715740 (tag
`v0.2.1`) failed, so Causals.jl has no GitHub Release. This PR fixes the
four defects behind that and hardens the workflow. A second, separate
commit stops tracking `Manifest.toml`; drop it on review if you prefer
to keep the file tracked.
**Commit 1: `fix(ci): make the release workflow build, attest and
publish`**
| Defect | Fix |
|---|---|
| Both upload steps pinned
`actions/upload-artifact@ea165f8d65b6db9a8b71b5c2d1a090c0daf9c8bb`, a
commit that does not exist. It is a corrupted copy of v4.6.2's
`ea165f8d65b6e75b540449e92b4886f43607fa02`. | Pinned to v7.0.1, which
SMTLib.jl already uses. |
| "Package artifacts" had no `shell:`, so `windows-latest` ran `mkdir
-p` and `tar` in pwsh. | The build job defaults to `bash`. |
| The SBOM step ran `using Pkg, JSON`. JSON is not a dependency and the
project was never instantiated. | Instantiate, read the direct
dependencies, then load JSON from a temporary environment. |
| The tracked `Manifest.toml` was written by Julia 1.13.0-alpha2
(manifest_format 2.1, with Causals listed as a path dependency of
itself). On 1.10, `Pkg.build()` fails with "Could not locate the source
code for the StyledStrings package". | Both jobs delete any committed
manifest and resolve from `[compat]`, which is how users install the
package. |
Hardening in the same commit:
- **A manual run builds the tag, not `main`.** In the build and SBOM
jobs, `actions/checkout` takes `ref: ${{ inputs.version &&
format('refs/tags/{0}', inputs.version) || '' }}`, so the tarballs match
the tagged tree that JPR registered.
- The ref is qualified as `refs/tags/` because checkout tries a bare
name as a branch before it tries a tag (`src/ref-helper.ts:52-57` at the
pinned SHA). Without the prefix, a branch with the same name as the tag
would be packaged instead.
- CodeRabbit raised this; it is fixed in commit 3 (`e536fcf`).
- On tag pushes and PRs, `inputs.version` is empty, so the ref is empty
and checkout uses the triggering ref.
- **The version is validated.** It is passed through `env:` rather than
inline `${{ }}`, must match `^v[0-9]+\.[0-9]+\.[0-9]+$`, and must
already exist as a tag (`git ls-remote --exit-code`). A typo cannot make
`action-gh-release` mint a new tag on `main`.
- **Least-privilege permissions.** `contents: read` at the top level.
The release job alone gets `contents: write`, `id-token: write` and
`attestations: write`.
- **Build provenance.** `actions/attest-build-provenance` v4.2.2 attests
`release/*`, matching SMTLib.jl's workflow.
- **The workflow tests itself at review.** A `pull_request` trigger on
this file runs the build and SBOM jobs. The release job is skipped on
PRs.
- `sha256sum -- *` (shellcheck SC2035, reported by actionlint).
**Commit 2: `chore: stop tracking Manifest.toml`**
`.gitignore:40` already lists `/Manifest.toml`, but the file has been
tracked since the first commit (`7fef2be`). CI on `main` is green only
because its `Pkg.add(url=…AcceleratorGate…)` step re-resolves the
manifest. I measured this: after that step `julia_version` reads 1.10.10
and the self-entry is gone. `STATE.a2ml` and `Dustfile.a2ml` still
describe the manifest drift. I left them as they are, because A2ML is
retired and is not edited.
No issue was open for this. I found it while checking why `v0.2.1` has
no Release.
**Commit 3: `fix(ci): check out refs/tags/<version> on a manual release
run`**
This commit qualifies the checkout ref in the build and SBOM jobs; the
first bullet under "Hardening" above explains why. CodeRabbit raised it
in review. The same fix is in hyperpolymath/SMTLib.jl#50 and
hyperpolymath/InvestigativeJournalism.jl#79.
## Type of change
- [x] 🐛 Bug fix (non-breaking change that fixes an issue)
- [ ] ✨ New feature: none. The `pull_request` trigger and attestation
serve the release path; they add no package functionality.
- [ ] 💥 Breaking change: the package source and `Project.toml` are
untouched. Commit 2 removes `Manifest.toml` from the index only.
- [ ] 🕳️ Soundness fix: not applicable, no checker or proof is involved.
- [ ] 📖 Documentation: no docs changed.
- [ ] 🧹 Refactor / tech debt: the manifest untrack is the nearest fit,
but it changes what Pkg resolves for a fresh clone, so it is not
behaviour-preserving.
- [x] 🔧 Build / CI / tooling
## 📌 New pins
**Head SHA: `e536fcf82a1709c45178547b45d874b2dbd8ed47`**
- **`actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a`**
(v7.0.1), used twice. It replaces the missing
`ea165f8d65b6db9a8b71b5c2d1a090c0daf9c8bb`.
-
**`actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8`**
(v4.2.2), new.
- Unchanged: checkout `3d3c42e5…`, setup-julia `fa02766e…` (v3.0.2),
julia-actions/cache `9353db09…` (v3.5.0), download-artifact `3e5f45b2…`
(v8.0.1), action-gh-release `efb35369…`.
I resolved all 14 `uses:` SHAs across this workflow and SMTLib.jl's and
InvestigativeJournalism.jl's release workflows through the GraphQL
`object(oid:)` lookup. 13 resolved to Commit objects. The 14th,
`ea165f8d65b6db9a…`, returned null, which is the positive control that
the lookup reports a missing SHA. This repo has no `actions.lock`, and
the workflow stays in block YAML: the KYAML preconditions in the
estate's AGENTS.md §2a are not met.
## How has this been verified?
- **`actionlint .github/workflows/release.yml`** passes (rc=0) after the
SC2035 fix, and again after commit 3.
- **Commit 3 (`e536fcf`, qualified tag ref).** I read the
branch-before-tag order from `src/ref-helper.ts` in `actions/checkout`
at the pinned SHA `3d3c42e5`. This PR's runs exercise only the empty-ref
path. The manual-run path is first exercised by the post-merge `gh
workflow run`, which publishes a Release, so I did not trigger it from
the PR branch.
- **Build and test on Julia 1.10.10, Linux x64**, from a fresh depot
with `Manifest.toml` removed. `Pkg.build(); Pkg.test()` passes,
including the 674 property tests. With the committed manifest, the same
command fails at `Pkg.build()` with the StyledStrings error above.
- **The SBOM script** run locally on 1.10.10 against the same tree
writes a CycloneDX 1.4 `sbom.json` with metadata Causals 0.2.1 and
components Graphs 1.15.0, Statistics 1.10.0, LinearAlgebra "stdlib",
Distributions 0.25.131 and StatsBase 0.34.13. The old `using Pkg, JSON`
form fails on an uninstantiated project.
- **This PR's own `Release` run** is the check for what I could not run
locally: the bash shell on `windows-latest`, macOS, and the pinned
actions resolving. The release job is skipped on it by design.
**Horizon:** I ran the Julia steps on Linux only, on 1.10.10. The
Windows and macOS legs, attestation, and Release creation are verified
only by CI, and Release creation only once a tag push or manual run
happens after merge.
## Checklist
- [x] My commits are **signed** (`git commit -S`). Both show `G`
locally.
- [x] I ran the project's own checks/tests locally and they pass
(1.10.10, Linux, manifest removed; see above).
- [x] New files carry the correct `SPDX-License-Identifier`: there are
no new files. `release.yml` keeps its `MPL-2.0` header.
- [x] Docs are updated, and no public claim now overstates what the code
does. The Release body now gives `gh attestation verify`, which this
workflow supports from this PR on. The stale A2ML notes are described
above.
- [x] I have not introduced a soundness hole.
## Notes for reviewers
- **After merge, publishing v0.2.1** is a manual run: `gh workflow run
release.yml -R hyperpolymath/Causals.jl -f version=v0.2.1`. It builds
the `v0.2.1` tag tree (`3bd2bac`), the same tree JPR registered.
- **Commit 2 is separable.** Without it, the workflow still works,
because it deletes the manifest before resolving. Local work on
1.10/1.11 keeps failing until the file is untracked.
- **Same broken template elsewhere.** The `using Pkg, JSON` SBOM step
appears in six other local repos (SMTLib, PolyglotFormalisms,
KnotTheory, HackenbushGames, Exnovation, BowtieRisk). SMTLib.jl gets its
own PR; the rest are recorded in `dev-notes/inbox/findings.md` for the
owner to triage, not filed as issues.
- Nothing is set to auto-merge.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016XJoh8YA2sCWQmq93CF8RX
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Tag
v0.1.1(2cc3088) has no GitHub Release, becauserelease.ymlwas the unfilled scaffold. Its build step only echoed"Build your artifacts here", so the SLSA job attested an empty subject list. It also downloaded git-cliff fromreleases/latest/download/git-cliff-x86_64-unknown-linux-gnu.tar.gz, which returns 404 today; git-cliff's asset names now carry the version. This PR replaces it with a workflow that tests the package, packages it, and publishes a Release.This is the same release-path fix as hyperpolymath/Causals.jl#69 and hyperpolymath/SMTLib.jl#50, adapted to this repo. No issue tracks it.
Changes
Build job:
hyperpolymath/julia-professional-registry(JPR). Causals, ZeroProb, Cliodynamics and their dependency AcceleratorGate are registered in JPR, not in General.Manifest.toml, so the package is resolved fresh from[compat], the way users install it.Pkg.instantiate(); Pkg.build(); Pkg.test().src/,Project.toml,README.adocandLICENSE.timeout-minutes: 45, matchingci.yml:30, because a cold install of this dependency closure is slow.SBOM: the build job writes a CycloneDX 1.4
sbom.json. It reads the direct dependencies by name from the resolved environment, then loads JSON from a temporary environment, because JSON is not a dependency of this package.Changelog:
orhun/git-cliff-actionv4.9.1, with git-cliff pinned to v2.14.2 instead of "latest". It uses the repo's existingcliff.tomlwith--latest --strip header.Provenance:
actions/attest-build-provenancev4.2.2 attests the actual release files (release/*, includingchecksums.txt). It replaces the SLSA generator, whose reusable workflow calls nested actions that cannot be pinned by SHA.A manual run releases an existing tag.
workflow_dispatchtakesversion. Both checkouts useref: ${{ inputs.version && format('refs/tags/{0}', inputs.version) || '' }}, so the artefacts and changelog come from the tagged tree. Therefs/tags/prefix is needed because checkout tries a bare name as a branch before it tries a tag (src/ref-helper.ts:52-57at the pinned SHA). It was added in238d922, after CodeRabbit raised the issue on the twin PR fix(ci): make the release workflow build, attest and publish Causals.jl#69. The release job then checks the version:env:, not inlined into the script;^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$;git ls-remote --exit-code).So a typo cannot make
action-gh-releasecreate a new tag onmain. A-presuffix marks the Release as a prerelease.Least privilege. The top-level permissions are
contents: read. Only the release job getscontents: write,id-token: writeandattestations: write.The workflow tests itself. A
pull_requesttrigger on this file runs the build and changelog jobs on this PR. The release job is skipped on PRs (if: github.event_name != 'pull_request').The unused
full-CHANGELOG.mdartefact is dropped, because nothing consumed it.📌 New pins
Head SHA:
238d922e711577f97b67ee613ee91525432a4351Each new or changed pin below was resolved from its tag to the same commit with a GraphQL
ref(qualifiedName:)lookup:julia-actions/setup-juliafa02766e078afaaf09b14210362cee14137e6a32julia-actions/cache9353db09a0f2bbbdec6f41791918c75d37391b36actions/upload-artifact043fb46d1a93c77aae656e7c1c64a875d1fc6a0aea165f8d…(v4.6.2)orhun/git-cliff-actiona9a95522b26fe6403f7bb24031f21fb573d0f5ffversion:input pins git-cliff v2.14.2actions/download-artifact3e5f45b2cfb9172054b4087a40e8e0b5a5461e7cactions/attest-build-provenance4d101475d8b20a2381f78447822ac1eab6504dd8Unchanged:
actions/checkout@8e8c483d…(v6.0.1) andsoftprops/action-gh-release@a06a81a0…(v2). Removed:slsa-framework/slsa-github-generator/…/generator_generic_slsa3.yml@f7dd8c54…(v2.1.0). This repo has noactions.lock. The workflow stays in block YAML, because this repo's gates do not yet meet the KYAML preconditions.RSR Quality Checklist
Required
just testor equivalent).Pkg.test()passes on thev0.1.1tree, which is this PR's parent commit; see Testing. This PR changes onlyrelease.yml.just fmtor equivalent). Not applicable: no Julia source changes. actionlint is the check that applies to the workflow.actionlint .github/workflows/release.ymlexits 0.unsafeblocks without// SAFETY:comments. Not applicable: no Rust or Zig code.believe_me,unsafeCoerce,Obj.magic,Admitted,sorry). None introduced.release.ymlkeeps itsMPL-2.0header; no new files..envfiles included. The workflow reads no secrets;action-gh-releaseuses the job'sGITHUB_TOKEN.As Applicable
.machine_readable/STATE.a2mlupdated. Not applicable: no project-state change. (The estate also no longer writes.a2mlfiles.).machine_readable/ECOSYSTEM.a2mlupdated. Not applicable, for the same reason..machine_readable/META.a2mlupdated. Not applicable, for the same reason.TOPOLOGY.mdupdated. Not applicable.CHANGELOGor release notes updated. Not ticked: release notes are generated per tag by git-cliff, and no changelog file is edited.licenseInfo).Testing
I ran the build job's steps locally, in a throwaway depot, on the
v0.1.1tree (git archive v0.1.1), with Julia 1.10.10 on Linux x64:Manifest.toml, andrm -f Manifest.tomlran anyway.Pkg.instantiate(); Pkg.build(); Pkg.test()gaveInvestigativeJournalism.jl174/174,E2E Pipeline Tests28/28 andProperty-Based Tests870/870. That is 1072 passed, 0 failed, 0 errored, rc=0.sbom.jsonparses as CycloneDX 1.4, with metadataInvestigativeJournalism 0.1.1and 18 direct-dependency components by name (Causals, Cliodynamics, ZeroProb, DataFrames, HTTP, …).Other checks:
actionlint .github/workflows/release.yml: rc=0, before and after238d922.238d922(qualified tag ref): I read the branch-before-tag order fromsrc/ref-helper.tsinactions/checkoutat the pinned SHA8e8c483d. This PR's runs exercise only the empty-ref path. The manual-run path is first exercised by the post-mergegh workflow run. That run publishes a Release, so I did not trigger it from the PR branch.cliff.toml,README.adoc,LICENSE,Project.tomlandsrc/.git-cliff --latest(local 2.13.1) on this tree produces the changelog, but warns that 15 commits were skipped due to parse errors. One commit message contains a literal\n\n, which shows as-is. Both arecliff.toml/commit-history issues that predate this PR, not workflow defects; see Notes.Horizon: Linux, Julia 1.10.10 and git-cliff 2.13.1, locally. CI on this PR runs the build and changelog jobs on the hosted runner. The release job, which checks the version, attests and publishes, cannot run on a PR. It is first exercised by the manual run after merge.
Screenshots
Not applicable.
Notes for reviewers
mainand checks out the tag, so the tag's oldrelease.ymldoes not matter:gh workflow run release.yml -R hyperpolymath/InvestigativeJournalism.jl -f version=v0.1.1ci.ymlinstalls them from git. The two can drift; this PR does not changeci.yml.cliff.tomlfollow-up (not fixed here): rungit-cliff -vv --latestto list the 15 skipped commits, then widencommit_parsersor add a catch-all.governance / Validate Hypatia Baseline. Tracked in governance / Validate Hypatia Baseline is red on main: 28 findings not covered by the baseline #80.maintoo (2cc3088, run 37864612160). There, 28 findings are not covered by.hypatia-baseline.json.release.yml, leaving 26, and adds none (job 113764445362).scan / gitleaks.🤖 Generated with Claude Code
https://claude.ai/code/session_016XJoh8YA2sCWQmq93CF8RX