Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modernize release CI workflow and include universal binaries and checksums #1486

Merged
merged 30 commits into from
Aug 2, 2024

Conversation

EliahKagan
Copy link
Member

@EliahKagan EliahKagan commented Aug 2, 2024

Closes #1478
Closes #1484

Major changes:

  1. Validate version against Cargo.toml unless specially named.
  2. Create release as a draft and publish after assets are all attached.
  3. Use gh for all release operations, eliminating deprecated actions (#1484).
  4. Make archives with macOS universal binaries (#1478 task 3; #1242 task 2).
  5. Generate and include SHA256 checksums.
  6. Document and refactor to avoid confusion.
  7. Make the workflow more readily testable? (done separately in #1499)

First… what it looks like

  • Here's an example release. (This attempts not to change anything related to release notes, and I deliberately have not edited the example release in any way.)
  • Here's its associated workflow run. Logs are available for each job. (Full information should be accessible to any logged-in user. Warnings from any job would be visible on that summary page as well, but there are no warnings anymore.)

To avoid confusion with real releases of gitoxide that people should actually use, I plan to delete that release eventually, but given its distinctive version "number," I'm not too worried and I'd be pleased to leave it up for at least as long as this PR is open and, if useful, possibly a bit longer.

If one wishes to do so, one can download all the files from that test release using gh:

mkdir tmp
cd tmp
gh release -R EliahKagan/gitoxide download v0.38.0-alpha.1-DO-NOT-USE

Each of the above checkboxed points ("tasks") is elaborated below, including the last one which I've written as unchecked because I'm not sure the changes here really achieve it. [Edit: It is now checked too, having been done later, in #1499.]

1. Validate version against Cargo.toml unless specially named

This is inspired by a check performed in the ripgrep workflow, though the behavior here is different to permit TEST-* and *-DO-NOT-USE and messages that report the nature of the likely mistake. Also, while I think this may not have been the case when ripgrep added this check, now the ubuntu-latest runner has the yq command, which accepts TOML input and therefore allows extracting the version from Cargo.toml to be done robustly.

2. Create release as a draft and publish after assets are all attached

This creates a draft release, which is only visible to users with push permissions on the repository. If any of steps (of any job) fail, it remains a draft release and the problem can be investigated. If they all succeed, then the last step of the last job marks it non-draft, publishing it.

This is directly adapted from the ripgrep workflow. The difference is that marking the release non-draft seems to be done manually there, rather than happening automatically. (For further discussion of the options related to this, see "Make the workflow more readily testable?" below.)

3. Use gh for all release operations, eliminating deprecated actions

This fixes #1484 by using gh instead of several actions related to operating on releases, including the upload-release-asset action, but also others that are not deprecated. This is for the reasons presented there, and as mentioned there, this also uses a (single) job output rather than GHA artifacts for passing metadata (now it is just a version) to subsequent jobs, thereby eliminating the use of actions related to GHA assets as well.

Although there are minor differences, and also more uses of gh because it is also used in the changes presented below, this is perhaps the area where the greatest benefit has coming from incorporating changes that appeared in the ripgrep workflow.

4. Make archives with macOS universal binaries

As recommended in #1478 (comment), this uses lipo to create archives with macOS Universal 2 binaries. There is one such archive for each feature.

It makes them by downloading the aarch64 and x86_64 archives for the same feature from the draft release and making a repack in which the ein and gix binaries are each combinations of both architectures' binaries.

Unfortunately, I did not find a way to make the dependencies between jobs as granular as I had hoped in #1478 (comment). I wanted to make them depend only on non-universal macOS and only on the ones for the same feature. It doesn't look like there is currently a supported way to do this with GitHub Actions using needs, and as far as I can tell it would not be made easier by using artifacts rather than the release itself to hold the assets.

It should be feasible to do it, or at least get part of the way there, using other techniques. In particular, the macOS jobs could be split off into a separate job definition, and substantial code duplication--which if present in this workflow would almost certainly lead to bugs, I think--could be avoided with a reusable workflow or composite action.

I decided not to try to do anything like that at this time. Instead, I just made the jobs that generate builds with universal binaries depend on all the jobs that generate the other builds. Unfortunately this means there is a substantial delay between when the architecture-specific builds are attached to the release and when the universal builds made from them are attached. There are two ways this could matter, though currently they are not very significant:

  • If the release goes back to being crated as a non-draft, i.e., published before everything is attached, then the delay could create the appearance to users looking for them that universal builds are not available.
  • If fail-fast: false is set in the build-release job definition so that jobs from that matrix are no longer cancelled automatically when others fail, then having the universal builds depend on the aarch64 and x86_64 builds could cause the universal builds not to be built when otherwise they would be able to.

5. Generate and include SHA256 checksums

The ripgrep workflow does this and I figured it would be a good idea. But this differs in a few ways:

  • Rather than publish with a separate file for each checksum, this initially creates the checksums that way and even attaches them to the draft release, but then once ready to publish, replaces them with a single hashes.sha256 files that lists all of them. I wasn't sure what filename to use; the name hashes.sha256 is inspired by that file in PowerShell release assets. If anything goes wrong in any step of any job up to that point, then the individual checksum files are retained in the unpublished draft.
  • Checksums are generated in binary mode explicitly. This will probably make no difference to the value, but it leaves no doubt. Also, it causes shasum to include the leading * before each filename in its output, attesting that the checksums were computed while treating the files that way.
  • shasum is used on all platforms, even Windows, because certutil produces output in a different format that, to the best of my knowledge, no common tools will automatically use. (The shasum command actually exists on the Windows GHA runners because Git for Windows ships it; it's just in a subdirectory that is not always in the PATH.) An example of a Rust project that uses this traditional Unix format for all archives including .zip archives for Windows targets is uv (though I did not use anything from its release workflow).

Although I think this approach is reasonable or I wouldn't be doing it, I also chose it because of its proximity to other approaches: this should not be too hard to change into any of the following if preferred:

  • Keep the individual .sha256 files and still include hashes.sha256.
  • Keep the individual .sha256 files and don't include hashes.sha256.
  • Never attach individual .sha256 files even while the release is a draft.
  • Use gpg to sign the hashes.sha256 file (would need another secret if automatic).

6. Document and refactor to avoid confusion

Overall the comments are actually sparer. But I placed one at the top of each job definition identifying what it does. I think that creating the release as a draft is also more intuitive, which does not mean we necessarily should keep it that way, but does mean that less confusion is likely when reading the workflow as long as it is the case.

The refactoring largely consists of changes to the way non-literal values get into script steps:

  • Sometimes, this is simply avoided by splitting steps up so that conditional logic is in if: keys guarding the steps.
  • For data from environment variables, access them with parameter expansion ($VAR) rather than by interpolating into the script with a template (${{ env.VAR }}).
  • For data not from environment variables, put them in environment variables scoped to the step in a step-level env:. (When non-sensitive and frequently used, put them in the job-level env:.)

In other scenarios--not this one--the latter two points would be necessary for security, because untrusted data can contain arbitrary characters including quotation marks. In this case, that is not a worry (except by accident), because none of the values are ever untrusted or otherwise especially unpredictable. So this is largely a stylistic matter. But:

  • It seems that the effect, in this case, is to increase readability. That is subjective.
  • Previous versions of this workflow had bugs that arose due to confusion about the semantics of access to env, such as the lifetime of variables inserted by appending to $GITHUB_ENV. I think this helps avoid that too.
  • Although secret redaction in GitHub Actions logs works just as well either way, having it be explicitly clear reading each step whether it can perform a privileged operation may be helpful to humans. Currently the only secret is github.GITHUB_TOKEN, but it does have write permissions.

Relating to the above, I had previously removed debug printing that didn't work because it attempted to print environment variables that were not set. I gave the workflow another look to see if maybe some of this information would be useful after all, and the ripgrep workflow helped too in what it prints. I brought some of that back, in subsequent steps so that the variables are set before being printed.

7. Make the workflow more readily testable?

Having this make releases initially as drafts seems beneficial in and of itself, in that the release can get into its final state before being made public, and in that the failure to produce or attach assets may sometimes mean the release should not be published without fixing something.

But the bigger reason I did it this way was that it enabled me to test this in my (public) fork, without creating a large number of releases that could potentially confuse people especially if they appeared in their GitHub "what's new" type feeds. (Though I did also name my versions with the string DO-NOT-USE, so most likely no one will be misled.) Only pretty near the end did I add the commit that enabled the last step of the new publish-release job.

But there are other possible approaches and some considerations the current approach may not adequately satisfy:

  • Maybe publishing should always be done manually.
  • Maybe it should be made public immediately like it was before.
  • Maybe considerations related to cargo-smart-release have bearing on this?
  • Maybe considerations related to manual editing of release notes have bearing on this?
  • Maybe it should work like this but have a way to keep it as a draft.
  • Maybe it should work like this but (also?) have a way to make it publish immediately (i.e. never be a draft).

Customization is pretty easy to add when a workflow is triggered by the workflow_dispatch event (here's an example). It is not always quite so easy in what is the more important case here of being triggered in push. But, for example, this could look at repository variables to determine some of its behavior. It could also have different behavior based on factors like whether it is running in a fork, though I would want that only to be a matter of different defaults so that testing changes to it in a fork could still be done robustly.

Of course, it can also be changed over time as preferences about how it should work become clear or change. It will also be changing in order to simplify things related to stripping debug symbols (#1477) and to publish releases for more targets or for some existing targets with more features (#1242). This PR satisfies the second point of #1242 (and #1479 satisfied the first), but otherwise this PR does not attempt to cover any of that.

This is inspired by the ripgrep workflow, though it currently does
it in a different way, since right now we are using actions rather
than the `gh` command.

Actual releases should of course become public. This can be done
manually; or another job, which depends on others, could be added
so it happens automatically if and after all builds succeed; or
this change to creating the releases as drafts (which at this
moment is mainly useful to facilitate testing without causing
confusion) could be reverted if no longer needed.
The variables that controlled the behavior of Rust programs and the
output of `cargo` are only used in the `build-release` job, not in
the `create-release` jobs, and one of them was already set in the
more suitable narrower scope. This moves them to that scope,
without duplication. They still apply to all steps in the build
jobs.

This also:

- Adjusts comment wording to clarify how `CARGO` is set and used.

- Quotes `1` as an environment variable value, since such values
  are always strings (it was being converted), but having it as a
  numeric YAML literal made it seem like that was not the case.

- Adds spacing for clarity and for stylistic consistency across the
  two job definitions.
This is strongly inspired by an analogous check in the ripgrep
workflow, but done differently (from how that currently does it) by
parsing `Cargo.toml` using `yq`.

The `yq` command supports reading TOML (just not writing it). This
command is present on the `ubuntu-latest` runner.

This also moves the "Show the version" step into the new step, and
has that step also show the version extracted from `Cargo.toml`, so
as to make errors easier to understand and also to help verify that
this logic is really working.
This way, real mismatches won't appear wrongly to be due to the "v"
that is now accounted for.
This permits names that start with `TEST-` or end with
`-DO-NOT-USE`, printing a different "OK" message for them.

The check comes after the comparison to the version in
`Cargo.toml`, which is still always looked up, for three reasons:

- Although it would be very weird and probably a bad idea to put a
  `-DO-NOT-USE` version in `Cargo.toml`, if it were ever done
  accidentally or on purpose, the message indicating a match to
  `Cargo.toml` should still be written.

- Having code paths that are only exercised for actual releases and
  rarely or never in testing is likely to lead to bugs.

- For looking it up and reporting it initially: this information is
  potentially valuable even when deliberately not used.

This commit also makes two other changes in that same script step:

- A custom message is now printed if the version is rejected only
  because it didn't have the "v" prefix (which this project's
  version tags and GitHub release names are using).

- Stylistic adjustment, mostly to match the quoting style used
  throughout the workflow.
This is mainly to avoid the effect of specifically recommending
that the release version/name (usually taken from a tag) be
changed to match the version in Cargo.toml, which would not always
be good advice because it may be that the version in Cargo.toml
is wrong, or that both are wrong, or in testing that a specially
named tag (or `VERSION` environment variable) should be used.
This is to keep the steps that produce that directory tree all
together.
We are already using a `$CARGO` environment variable, which is
mainly for convenient expansion by the shell, but `cargo` (and
commands like `cross` with the same interface) pass it on to build
scripts, which may use it (and if or how they do may in principle
vary by feature or target). But the sitation with `$TARGET` is
analogous--it would also make commands more readable, and it is
also passed down to scripts by `cargo`.

So this adds `$TARGET`.

Doing this serves another purpose, which is to make it easier to
reason about the semantics of the commands the shell is running.
Using `${{ }}` interpolation should not be a security risk here,
since all values are trusted. But injecting characters such as `'`
could still happen by accident. Often it may not be justified,
outside of reusable workflows or those running on events with
elevated security risks, to route them through environment
variables to ensure their contents are not interpreted specially
by the shell. However, with the addition of `$TARGET`, it seems
that most of that has already been done, such that clarity is
overall improved rather than worsened by going the rest of the way.

So this does that too, adding other environment variables in the
narrowest scope that is broad enough to avoid duplication. Now all
`${{ }}` interpolations are outside of script code. Note that
these changes only apply to the release workflow and may not
necessarily be justified in other workflows.
- Slightly reword and reformat the block comment.

- Run two commands instead of using `&&`, since with `shell: bash`
  (inherited as the default as specified at the workflow level),
  the actual shell command invoked for script steps includes `-e`,
  so the script would still immediately fail if the first command
  fails.
This makes substantial changes to the release workflow, most of
them straightforwardly adapted from corresponding material in the
ripgrep release workflow:

- The biggest change is to use `gh` (the GitHub CLI) instead of
  both the ncipollo/release-action and actions/upload-release-asset
  actions.

- Use outputs instead of artifacts for the information that needs
  to go from the `create-release` job into the `build-release`
  jobs. This eliminates the need for actions/upload-artifact and
  actions/download-artifact.

  Furthermore, since `gh` doesn't require a URL to add files to an
  existing release, there is only one output, the version.

- Split up the "Build archive" step so it doesn't need awkward
  conditional logic inside a single script step. Now the platform
  agnostic part of creating the directory and putting documentation
  in it is one step, followed by steps with `if:` keys for Windows
  and Unix.

  For this, the main differences from how it is currently written
  in the ripgrep workflow are the step titles, the uses of shell
  expansion rather than `${{ }}` interpolation for the environment
  variables, and the omission of checksum files since we are not
  currently generating those.

This notably does not add either of the following to the workflow:

- This does not set `permissions:` for the workflow. It was not set
  before, so the configuration, including in the upstream repo,
  seems not to require it. (Note that this does not imply that the
  configuration in the ripgrep repo doesn't require it.)

- This does try to do anything explicit to take the place of
  specifying `omitBody: true` for ncipollo/release-action. I'm not
  sure what should be done for this, but the current behavior seems
  to produce the same result, and passing `--notes ''` to `gh`
  might go too far. The current ripgrep workflow has no explicit
  argument corresponding to this.
This expands the stub job for making Universal 2 binaries for macOS
into a full definition, downloading and extracting the two
architecture-specific archives from the GitHub release. This may
require further refinement.

It also refactors the way environment variables are set for the
preexisting jobs:

- Order variables in `env:` so they may be easier to understand.
- Define `VERSION` in `env:` rather than in its own step.

I think this is a bit more readable, but also, it allows the new
job to be stylistically consistent with the preexisting jobs.
- Add missing `GITHUB_TOKEN` for download.

  This is neeeded even for the download step because the release is
  a draft.

  (Even if that is changed, it may be made a draft manually during
  creation under some conditions, which would not usually signal a
  wish that the Universal 2 binary archive specifically be
  omitted.)

- Break up into more steps.
This adds a `REPOSITORY` environment variable in at the job level
for build-macos-universal2-release and references it in the two
steps that apparently need it.

Although these are the same two steps that use the token, that is
not added to steps that don't require it, since it is sensitive.
As commented, this should catch if the features lists get out of
sync. (It may catch other problems too, but without this issue, the
check is likely not justified. Printing the list of artifacts at
the time publishing is about to occur may still be justified in
that case, though.)
This uses the code from the ripgrep workflow to do so, with small
modifications to fit the style used here, and, except for the code
that is specific to Windows, occurring twice: once for most of the
Unix jobs, and once for the macOS Universal 2 archive.

This also makes these closely related changes:

+ Refactor the parts of the Universal 2 job that are similar to the
  other jobs so they are expressed more similarly.

+ Check the new checksums for the `gh release download` downloaded
  archives that the Universal 2 job takes its architecture-specific
  binaries from (to combine into an universal binary). The risk
  that the files would be corrupted when downloaded in this way is
  *extremely* low, but the presence of a checksum published for the
  Universal 2 archive might be interpreted to mean that downloaded
  archives used for the constituent binary images were verified.

  (As done here, this verification is not really for security,
  since the checksums used to do it are obtained from the same
  source in the same way -- which fortunately is pretty secure.
  It may safeguard against a very small risk of corruption. It also
  fails earlier if the files are not downloaded at all, in case the
  cause is not one that caused `gh` to exit with a failure status.)
With `shasum` for Unix checksums, they look like:

    407860b1605577700750b92f464068fdaa65ff5ecb7fabcd5a9ba8dac7156149  gitoxide-max-pure-v0.38.0-alpha.2-DO-NOT-USE-x86_64-unknown-linux-musl.tar.gz

With `certutil` for Windows checksums, they looked like:

    SHA256 hash of gitoxide-max-pure-v0.38.0-alpha.2-DO-NOT-USE-x86_64-pc-windows-msvc.zip:
    870a157307d8674f981278afa2161973d65a4c6956fc2810cdc901886a41da12
    CertUtil: -hashfile command completed successfully.

Unlike `shasum`, the `certutil` command does not verify checksums,
it only generates them. As far as I know, there are no common tools
that require the format to be as `certutil` outputs it. In
contrast, tools commonly expect the format `shasum` outputs.

Furthermore, the Git Bash environment from Git for Windows includes
`shasum`, which means it is present:

- On GitHub Actions runners for Windows (as for other platforms).
- On the computers of most Windows users in gitoxide's user base.

Even if someone does not have `shasum` or another tool that will
automatically verify checksums from this format, they would at
worst need to verify it manually, which I believe is typically
already the case when examining output from `certutil` in the above
format. Furthermore, we have not published checksums before, so for
gitoxide no one is relying on checksums being published that way.
All files whose checksums are computed here are binary files, and
even if `shasum` were to behave the same, it omits the `*` prefix
in front of the filename that allows humans and tools to know for
sure that it was binary mode unless `--binary` is passed.
To avoid setting `GITHUB_TOKEN` where not needed, this defines it
in `env:` for each individual step that needs it, as elsewhere.
This is more awkward than in the other jobs, since half the steps
use it here, but this may be okay.
Since they are all exactly the same as the job IDs.
This is to account for recent additions that are not present in the
history of the ripgrep workflow, so if there are problems with them
then people don't spend too much time trying to figure out what in
the ripgrep workflow they would have come from.
@Byron
Copy link
Member

Byron commented Aug 2, 2024

Thank you so, so much for tackling this!

Here is my thoughts and comments related to the various points.

1. Validate version against Cargo.toml unless specially named

Neat, appreciated!

2. Create release as a draft and publish after assets are all attached

That is a major improvement, thank you!

3. Use gh for all release operations, eliminating deprecated actions

That is great to hear, gh has been very stable and could hardly be more trusted to keep working like for some time to come, greatly improving the reliability of the workflow.

4. Make archives with macOS universal binaries

This is fantastic and a great achievement, thank you!

It should be feasible to do it, or at least get part of the way there, using other techniques. In particular, the macOS jobs could be split off into a separate job definition, and substantial code duplication--which if present in this workflow would almost certainly lead to bugs, I think--could be avoided with a reusable workflow or composite action.

I had touched points with these over at GitButler and didn't particularly like them for the added level of indirection. But then again, I also don't like YAML based things so I am biased. However, if you think the complexity is worth it and feel like it should be done, I'd absolutely go with it if it manages to avoid code duplication. Personally, I am fine with the trade-offs as is, as I assume builds work most of the time now.

5. Generate and include SHA256 checksums

Thank you, I am absolutely happy with the choices made.

Something I'd find helpful is if the filename would reveal information on either how it was created or how users can validate the hash. I suppose the suffix already does it, but I wonder if naming it shasum-hashes.sha256 would help, maybe along with a ready-made shasum-hashes.md to learn more on how these should be used and most importantly, validated. Those who know could already do it, but those who don't probably wouldn't and maybe the UX around this can be improved trivially.

If you agree, this could be a follow-up PR.

6. Document and refactor to avoid confusion

I just read through the new file start to end (without using the diff-view), and can confirm that it makes a clean and much simpler impression, with all steps clearly marked. Great work!

There was the ubuntu-latest-disabled comparison that explained itself with a comment, but since I read it after the fact I was initially confused about the purpose. My expectation would have been that it's commented out, but that comes with its own issues so probably doing it like this is overall better. My initially confusion is due to not being used to it, and I am happy to learn and adapt.

7. Make the workflow more readily testable?

  • Maybe publishing should always be done manually.
    • I am glad I don't have to :).
  • Maybe it should be made public immediately like it was before.
    • It seems objectively better to start it out as draft.
  • Maybe considerations related to cargo-smart-release have bearing on this?
    • I am glad it has nothing to do with it.
  • Maybe considerations related to manual editing of release notes have bearing on this?
    • Once gix and gitoxide go v1.0, I think the approach to top-level changelogs will be revised. In theory though, this can be done as part of CHANGELOG.md and manual edits to it (which are fully supported), so maybe pretty changelogs work already. They will naturally end up in the release.
  • Maybe it should work like this but have a way to keep it as a draft. Maybe it should work like this but (also?) have a way to make it publish immediately (i.e. never be a draft).
    • I don't see an application for this yet, probably that's an indicator that it's not (yet) needed.

Of course, it can also be changed over time as preferences about how it should work become clear or change.

I agree, let's go with that.


💜Lovely, thanks again 🙏

@Byron Byron merged commit e0420c0 into GitoxideLabs:main Aug 2, 2024
19 checks passed
@EliahKagan EliahKagan deleted the releasing branch August 2, 2024 16:08
@EliahKagan
Copy link
Member Author

EliahKagan commented Aug 4, 2024

Thank you so, so much for tackling this!

No problem!

4. Make archives with macOS universal binaries

This is fantastic and a great achievement, thank you!

This is thanks to @NobodyXu for suggesting it and explaining how to use use lipo in #1478 (comment)!

I had touched points with these over at GitButler and didn't particularly like them for the added level of indirection. But then again, I also don't like YAML based things so I am biased.

There are ways to bring in static type checking and even a different syntax. I don't know how much that would help or if it would be worthwhile. I'll try and open a discussion question about it soon.

However, if you think the complexity is worth it and feel like it should be done, I'd absolutely go with it if it manages to avoid code duplication.

At this point, I think I would only want to do it if it does not increase complexity. Even then, my main reluctance is that putting code that really is tightly coupled to this workflow in another file could make it harder to understand what is going on, even if the combined length and complexity of both files were to decrease.

Something I'd find helpful is if the filename would reveal information on either how it was created or how users can validate the hash. I suppose the suffix already does it, but I wonder if naming it shasum-hashes.sha256 would help, maybe along with a ready-made shasum-hashes.md to learn more on how these should be used and most importantly, validated. Those who know could already do it, but those who don't probably wouldn't and maybe the UX around this can be improved trivially.

One problem is that I don't think I actually know how people should validate the hash, other than that they should use whatever program their system has for validating SHA256 checksums provided in a file formatted in this traditional way. I think most Unix-like systems have such a command, but the best command to do so may differ across systems. The available command for this is not always shasum, though when it is not, I think it is usually sha256sum.

The workflow uses the shasum command because it is present on all GitHub-hosted GHA runners. (In ripgrep, the workflow used certutil on Windows to generate the hashes in a different format. But it used shasum rather than sha256sum probably because, of the two, only shasum is shipped with macOS.) shasum is a perl script that may be absent if perl is not installed, and that I think may also be absent if perl is installed but in a minimal way. It may also be in an unusual location even if it is installed, as is the case on the Windows runners (where the workflow hard-codes the path).

One might think that perl would always be present, since some git commands use it. This is, after all, why a full installation of Git for Windows has it (in contrast, MinGit does not provide perl). But there are systems on which this is not the case, but where one might reasonably use or even develop gitoxide. In particular, my Alpine Linux 3.17 system has git but not perl, and all but one of the gitoxide unit tests pass. It does not have shasum, but it does have sha256sum. Its sha256sum is provided by BusyBox. GNU/Linux systems have sha256sum from GNU coreutils. The BusyBox sha256sum and GNU coreutils sha256sum differ in some ways; for example, both support -c, but the BusyBox implementation does not recognize long options so does not accept --check.

Most Linux-based systems that intentionally support command-line use will therefore have sha256sum one way or another. Furthermore, Git for Windows also ships sha256sum, since its Git Bash environment includes a port of GNU coreutils. (In light of this, I should very possibly have used that command to generate the hashes on Windows rather than hard-coding /usr/bin/core_perl/shasum. I'll try to look into which is better next time I make changes to the workflow.) It may mainly be macOS where shasum is more likely to be present than sha256sum.

If you agree, this could be a follow-up PR.

The above is mostly a problem for naming the file in a way that makes clearer how it should be used. It is not necessarily a problem for giving instructions, which could simply say something like, "On most systems, you can use shasum -c or sha256sum -c to check the hashes," and maybe also something about Git Bash.

However, I am not sure it makes sense for a file with those instructions to be among the release files. It seems like this belongs in the top-level readme, or a separate top-level file giving or reiterating installation instructions. The disadvantage of this is that if the way the hashes are provided changes over time, then there could be skew between the version of the documentation that a user is reading and the version that would apply to the download the user is doing.

Some projects repeat the hashes of downloadable archives in the release notes and give instructions. If that is done, then this makes sense there. Or the release notes could mention the file of hashes and say how to check them, or link to information about how to check them. But I don't know if either of these things is worthwhile.

There is another consideration that may be important. Providing a single file of hashes may be confusing to users who have not often checked hashes, because running shasum -c or sha256sum -c (or other compatible commands, if any) on it will report the absence of any files that were not downloaded. Maybe this is the reason some projects provide separate .sha256 files for each archive.

Whatever is done for this, the publish-release job can shortened based on it. Right now, its check for feature consistency is written separately from its consolidation of the checksums. I did this deliberately to avoid committing to a particular approach, but with the idea of eventually combining work that gives overlapping results.

  • If the approach of doing them independently is desired then it can be reinforced by splitting the job into multiple, probably just two, jobs. That may be better than just leaving things as they are. The clean up step in the middle is a strong signal that this one job is doing too much. (That step is actually not needed, but it makes the job easier to reason about because the reader need not verify that files with .txt suffixes are never used after that point.) But using fewer jobs also makes things run slightly faster.
  • If we keep the current approach of replacing the individual checksum files with one file, then we don't need to download the asset list as JSON and then operate on it, because we have one checksum per archive and we can just check for feature consistency in the names of those downloaded files.
  • If we don't keep that approach, then the code that replaces the individual checksum files with one file can be removed, and the rest left in place.

6. Document and refactor to avoid confusion

I just read through the new file start to end (without using the diff-view), and can confirm that it makes a clean and much simpler impression, with all steps clearly marked. Great work!

Thanks--this is tricky to be sure of, and your appraisal increases my confidence that the stylistic changes have helped, or at least not hurt.

There was the ubuntu-latest-disabled comparison that explained itself with a comment, but since I read it after the fact I was initially confused about the purpose. My expectation would have been that it's commented out, but that comes with its own issues so probably doing it like this is overall better. My initially confusion is due to not being used to it, and I am happy to learn and adapt.

This was actually there before, with the same ubuntu-latest-disabled check (which was added in 28d2cf5). The only change I made to that step was to try and improve its readability in f7ee365.

I didn't do anything further with it in this PR, because it is something I hope to remove altogether soon, when working on #1477 and adding more Linux targets and features in the spirit of #1242. I don't think the packages listed there would be sufficient to do everything we want to do with musl. I'm also not sure any such command could be sufficient, since cross is being used for all Linux builds, and headers and build tools need to be inside or otherwise accessible from the Docker containers it uses.

I tested max locally in an Alpine Linux 3.17 x86-64 system (since Alpine uses musl instead of glibc) to investigate what those comments as well as the related ones on the feature exclusions seem to indicate are the blockers:

  • I was able to produce what I think are the problems referenced there when attempting to use OpenSSL, by building with the openssl and openssl-dev packages installed and no LibreSSL-related packages installed. The linker reported cannot find -lssl and cannot find -lcrypto. Maybe there are other packages that would rectify this while still using OpenSSL with musl, though the libssl3 and ssl_client packages were also (already) installed.

  • But when I uninstalled openssl and openssl-dev and installed libressl-dev--which also pulled in libressl3.6-libcrypto, libressl3.6-libssl, and libressl3.6-libtls as dependencies--I was able to build with no errors. Both 0.37.0 and the current tip of the main branch build with no problem.

    On main, I ran all tests with cargo nextest run --all --no-fail-fast. Without bash, there were over 100 failures, since some of the tests make use of it, but this is similar to what happens on a FreeBSD system without bash and is not related to the available libraries. With bash installed, only one test failed on Alpine Linux, from_path_no_follow. This happens both before and after the changes in #1488 and is due to the BusyBox touch command not accepting that syntax (I think it doesn't accept nanoseconds).

    On Ubuntu 22.04 LTS, running busybox touch -d "2446-05-10 22:38:55.111111111" future produces the same invalid date error, so the problem relates to BusyBox vs. GNU coreutils but does not relate to musl vs glibc. The touch invocation is in a fixture whose generated archive is explicitly .gitignored and that is therefore run on any system. The code under test is not itself invoking touch and does not itself seem to have any problems.

  • As requested in #1242, a third option would be to use neither library by having gitoxide use rustls instead.

In view of that, it seems to me that it may be possible to make and publish musl builds for all features that we are building other targets for, without changing any code of gitoxide or even its dependencies, by installing LibreSSL libraries and headers in the environment cross uses. I would want check if such builds work on a variety of systems, though. If not, then it may be necessary to change some configuration to link LibreSSL statically.

If this problem can be solved soon--even if not by the ultimately solution which is probably rustls--then I think further changes to the step with the ubuntu-latest-disabled comparison could wait until then.

@NobodyXu
Copy link
Contributor

NobodyXu commented Aug 4, 2024

For the signature part, I'd recommend to use minisign as cargo-binstall can use it to verify identity of the publisher.

The schema I recommend is to generate a new key-pair during publishing, write the private key into Cargo.toml, and then publish it.

For the private key, it would be used to sign the binary artifacts, and then be destroyed.

In cargo-binstall release PR, we achieve this by setting up another key-pair, use it to encrypt the temporary private key, and upload it as artifacts.

Once all package building/signature is done, we delete the private key.

@Byron
Copy link
Member

Byron commented Aug 4, 2024

Thanks for the suggestion, @NobodyXu , I like the idea of huddling around a standard that is already supported by a tool tasked with installing software, it's a great match.

There is certainly complexities involved due to the key handling, but I'd hope it's exclusively during artifact production in this workflow. Otherwise, cargo smart-release would have to learn it as it is tasked with publishing, and it seems like a lot can go wrong especially if a private key for a published version has to be re-used for artifact creation. Admittedly, I don't claim I understand the workflow and I didn't look it up.

So all in all, maybe aligning around how influential projects do it might be more suitable as first step.


There are ways to bring in static type checking and even a different syntax. I don't know how much that would help or if it would be worthwhile. I'll try and open a discussion question about it soon.

The IDE can certainly provide more support, but I don't know if this is anything that can be improved by changing the file. I suppose it's more about which plugins are installed. Probably I am missing something, apologies.

If this problem can be solved soon--even if not by the ultimately solution which is probably rustls--then I think further changes to the step with the ubuntu-latest-disabled comparison could wait until then.

It's exciting to hear that this could go away and the MUSL builds could work with the max setting, and I am looking forward to whichever changes come first. If ubuntu-latest-disabled goes away in the process, even better.

@NobodyXu
Copy link
Contributor

NobodyXu commented Aug 4, 2024

There is certainly complexities involved due to the key handling, but I'd hope it's exclusively during artifact production in this workflow.

It depends on how you generate the minisign key-pair.

If you generate it once and stored the private key within repo secrets and write the public key to Cargo.toml and commit it, then yeah, it would only affect artifact production.

If you generate one per release like cargo-binstall does, then you'd have to change cargo smart-release:

It would have to generate a key-pair, write the public key into Cargo.toml before publishing, and the private key would be encrypted and uploaded to action artifact for signing (removed after building is completed).

We did this because crates.io is guaranteed to be immutable, whereas github release can be modified at anytime.

By generating a unique public key for each release and putting it into Cargo.toml, even if someone breaks one of the release, other is still fine.

@Byron
Copy link
Member

Byron commented Aug 4, 2024

Thanks for elaborating. I think for one, I think minisign is an upgrade to just providing hashes as it can potentially validate authorship, hashes can only validate the file one was supposed to download was actually downloaded.

Having a per-release key that is thrown away seems to invalidate the idea of proving authorship, as it's an ever-changing public key that anyone could have created. Maybe this is also where I am wrong or misunderstand things.

@NobodyXu
Copy link
Contributor

NobodyXu commented Aug 4, 2024

Having a per-release key that is thrown away seems to invalidate the idea of proving authorship, as it's an ever-changing public key that anyone could have created.

The idea is to minimize the risk of malicious takeover.

By having a key for each release, stored on crates.io, we guarantee that even if attacker takeover the account/repository, they can't modify the existing release artifacts without getting noticed.

Sure they can create new release, but they can't modify existing releases and put something into it.

@Byron
Copy link
Member

Byron commented Aug 4, 2024

Ah, right, and crates.io stores the hashes it has seen so cargo-binstall can double-check it from there.
This also sounds like any hash could be used to do the same, as along as a trusted third party stores what it has seen, but using cryptographic keys seems like it's more secure and versatile.

@NobodyXu
Copy link
Contributor

NobodyXu commented Aug 4, 2024

Ah, right, and crates.io stores the hashes it has seen so cargo-binstall can double-check it from there.

Yes we already check that in cargo-binstall

This also sounds like any hash could be used to do the same, as along as a trusted third party stores what it has seen, but using cryptographic keys seems like it's more secure and versatile.

We currently only support crypto asymmetric keys in the Cargo.toml.

Storing the key in github release makes no sense at all, since it can be overwritten by malicious takeover.

We also have plans to support more algorithms

cargo-bins/cargo-binstall#1

@EliahKagan
Copy link
Member Author

EliahKagan commented Aug 11, 2024

There are ways to bring in static type checking and even a different syntax. I don't know how much that would help or if it would be worthwhile. I'll try and open a discussion question about it soon.

The IDE can certainly provide more support, but I don't know if this is anything that can be improved by changing the file. I suppose it's more about which plugins are installed. Probably I am missing something, apologies.

Yes, for VS Code there is the GitHub Actions extension. But I'm suggesting something else: generating them from a DSL written in a statically typed language. This may be a hard sell because, as far as I can tell, currently no one has made anything where Rust would be that language. I've opened #1512 where I have described this in more detail.

If this problem can be solved soon--even if not by the ultimately solution which is probably rustls--then I think further changes to the step with the ubuntu-latest-disabled comparison could wait until then.

It's exciting to hear that this could go away and the MUSL builds could work with the max setting, and I am looking forward to whichever changes come first. If ubuntu-latest-disabled goes away in the process, even better.

Due to #1493, I am not sure this can be solved the way I had hoped. Since the step with ubuntu-latest-disabled does not likely resemble anything that would be used when this is fixed (because we are using cross for all Linux builds), I am inclined to go ahead and remove it soon anyway, possibly when making changes discussed in #1498.


Regarding signing releases as discussed above, is there anything I can or should do to help out with that?

@Byron
Copy link
Member

Byron commented Aug 12, 2024

Regarding signing releases as discussed above, is there anything I can or should do to help out with that?

I'd leave it as is, but go back to that once gitoxide is stable or plans to become stable. Then supply-chain safety would have an even higher value.

The BoM already interests me a lot though, and if that interests you as well maybe that could be investigated for the purpose of adding it to GitHub releases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants