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

feat: macos signing and notarization #367

Merged
merged 19 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 111 additions & 16 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,130 @@ on:
pull_request:
branches:
- master
workflow_dispatch:
inputs:
dist_root:
description: 'DIST_ROOT'
required: true
default: '/ipns/dist.ipfs.io'

env:
DIST_ROOT: ${{ github.event.inputs.custom_dist_root || '/ipns/dist.ipfs.io' }} # content root used for calculating diff to build
GO_IPFS_VER: 'v0.9.1' # go-ipfs daemon used for chunking and applying diff
CLUSTER_CTL_VER: 'v0.14.0' # ipfs-cluster-ctl used for pinning
Comment on lines +19 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the process for updating these? Do we rely on someone manually updating it? What's their trigger for doing so?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those are low importance: could be kept as-is until someone cares to update or there is a breaking change that forces version bump.

Having static versions of build tools makes the builds more robust and reproducible, but we could also switch to always run on the latest version for dogfooding – don't feel strongly either way, but leaning towards more reproducibility.


jobs:
build:
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
- uses: actions/setup-node@v2
with:
node-version: '14'
- uses: actions/setup-go@v2
- name: Setup IPFS
run: ./scripts/ci/setup-ipfs.sh
env:
CLUSTER_USER: ${{ secrets.CLUSTER_USER }}
CLUSTER_PASSWORD: ${{ secrets.CLUSTER_PASSWORD }}
timeout-minutes: 5
- name: Build any new ./releases
run: ./dockerized make all_dists
- name: Inspect git status and contents of ./releases
run: git status && ls -Rhl ./releases
- name: Temporarily save ./releases artifacts
uses: actions/upload-artifact@v2
with:
go-version: '1.16'
- run: sudo snap install ipfs jq
- run: ipfs init --profile server
- run: ipfs daemon &
- name: Wait for ipfs daemon
run: npx wait-port http://127.0.0.1:8080/api/v0/version
- name: Connect to ipfs cluster
run: ipfs swarm connect /dnsaddr/cluster.ipfs.io
- run: make publish
# todo: add $(cat versions) to cluster (and wait)
# todo: update dist dnslink if changed.

lint:
name: releases-unsigned-diff
path: releases
retention-days: 1

lint:
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm ci --no-audit --progress=false
- run: npm run lint

sign-macos:
runs-on: "macos-latest"
needs: build
steps:
- uses: actions/checkout@v2
- name: Retrieve unsigned artifacts
uses: actions/download-artifact@v2
with:
name: releases-unsigned-diff
path: releases
continue-on-error: true # skip if no releases
- name: List ./releases before
run: ls -Rhl ./releases || echo "No ./releases"
- name: Install gon via HomeBrew for code signing and app notarization
run: |
brew tap mitchellh/gon
brew install ipfs coreutils gawk gnu-sed jq mitchellh/gon/gon
Comment on lines +72 to +73
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ gon package is used by Terraform project, so multiple eyes are looking at this

ipfs init --profile test # needed for calculating NEW_CID later
- name: Import Keychain Certs
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1@2020-02-03
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 👮‍♂️ I hardcoded specific revision because this is a third-party action and in theory someone could swap-out git tag and steal our signing keys.

with:
p12-file-base64: ${{ secrets.APPLE_CERTS_P12 }}
p12-password: ${{ secrets.APPLE_CERTS_PASS }}
- name: Verify identity used for signing
run: security find-identity -v
- name: Sign any new releases
run: ./scripts/ci/sign-new-macos-releases.sh
env:
WORK_DIR: ${{ github.workspace }}
AC_USERNAME: ${{ secrets.APPLE_AC_USERNAME }} # implicitly read from env by gon
AC_PASSWORD: ${{ secrets.APPLE_AC_PASSWORD }}
- name: List ./releases after
run: ls -Rhl ./releases || echo "No ./releases"
- name: Temporarily save notarized artifacts
uses: actions/upload-artifact@v2
with:
name: releases-signed-macos-diff
path: releases
retention-days: 1
continue-on-error: true # skip if no releases

persist:
runs-on: "ubuntu-latest"
needs: sign-macos
steps:
- uses: actions/checkout@v2
- name: Retrieve signed artifacts
uses: actions/download-artifact@v2
continue-on-error: true # skip if no releases
with:
name: releases-signed-macos-diff
path: releases
- name: List ./releases
run: ls -Rhl ./releases || echo "No ./releases"
- name: Setup IPFS
run: ./scripts/ci/setup-ipfs.sh
env:
CLUSTER_USER: ${{ secrets.CLUSTER_USER }}
CLUSTER_PASSWORD: ${{ secrets.CLUSTER_PASSWORD }}
timeout-minutes: 5
- run: ./dockerized make publish
- run: git status
- name: Read CID of updated DAG
id: cid-reader
run: echo "::set-output name=CID::$(tail -1 ./versions)"
- name: Pin new website to ipfs-websites.collab.ipfscluster.io
run: ./scripts/ci/pin-to-cluster.sh
env:
PIN_CID: ${{ steps.cid-reader.outputs.CID }}
PIN_NAME: "https://github.com/ipfs/distributions/commits/${{ github.sha }}"
PIN_ADD_EXTRA_ARGS: ""
CLUSTER_USER: ${{ secrets.CLUSTER_USER }}
CLUSTER_PASSWORD: ${{ secrets.CLUSTER_PASSWORD }}
timeout-minutes: 60
- name: Update PR status with preview link
run: ./scripts/ci/github-preview-link.sh
env:
CONTENT_PATH: "/ipfs/${{ steps.cid-reader.outputs.CID }}/"
GIT_REVISION: ${{ github.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
FROM ubuntu:20.04
ARG USER_UID
ARG GO_IPFS_VER
RUN apt-get update -q && apt-get install -y git curl gnupg jq build-essential gawk zip
RUN curl -s https://dist.ipfs.io/go-ipfs/v0.8.0/go-ipfs_v0.8.0_linux-amd64.tar.gz | tar vzx -C /usr/local/bin/ go-ipfs/ipfs --strip-components=1
RUN curl -s "https://dist.ipfs.io/go-ipfs/${GO_IPFS_VER}/go-ipfs_${GO_IPFS_VER}_linux-amd64.tar.gz" | tar vzx -C /usr/local/bin/ go-ipfs/ipfs --strip-components=1

RUN adduser --shell /bin/bash --home /asdf --disabled-password --gecos asdf asdf --uid $USER_UID
ENV PATH="${PATH}:/asdf/.asdf/shims:/asdf/.asdf/bin"
Expand Down
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@
- [Adding a new (go) distribution](#adding-a-new-go-distribution)
- [Publishing](#publishing)
- [Background](#background)
- [Notes on reproducible builds](#notes-on-reproducible-builds)
- [Contribute](#contribute)
- [Want to hack on IPFS?](#want-to-hack-on-ipfs)
- [License](#license)

## Install

Clone the repo and install the following dependencies via your favorite package manager:
Clone the repo and use Docker via `./dockerized <cmd>` wrapper.

If you don't want to run `./dockerized` build, install
the following dependencies via your favorite package manager:

* `go`
* `npm` (v7.13.0+ with nodejs v16.2.0+)
Expand Down Expand Up @@ -83,13 +87,20 @@ Run:
> ./dist.sh add-version <dist> <version>
```

This will add the version to `dists/<dist>/versions`, set it as the current version in `dists/<dist>/current`, and build it.
This will add the version to `dists/<dist>/versions`, set it as the current version in `dists/<dist>/current`, and build it locally.

Example:
```sh
> ./dist.sh add-version fs-repo-99-to-100 v1.0.1
```

To produce a signed, **official build** for use in DNSLink at `dist.ipfs.io`:

1. Run `./dist.sh add-version` locally.
2. Commit created changes to `dists/<dist>` and open a PR against `ipfs/distributions`.
3. Wait for Github Action to finish PR build. It runs `./dockerized` build, then signs macOS binaries and spits out updated root CID at the end.
4. If everything looks good, write down the CID from the preview link on the PR, and update the DNSlink at `dist.ipfs.io`.
Comment on lines +97 to +102
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ I believe this is the TLDR of this entire PR :)


### Adding a new (go) distribution

Run:
Expand All @@ -107,25 +118,27 @@ The optional `sub_package` argument is used to specify a module within a repo.

### Publishing

In the root of the repository, run:
To produce a CID (`<NEW_HASH>`) that includes binaries for all versions defined in `./dists/`, in the root of the repository, run:

```sh
> make publish
```

This will build any new binaries defined by dist and the website to the `releases` dir, add it to ipfs and patch it into the existing dag for the published dist.ipfs.io. Save the hash it spits out (we'll call it `<NEW_HASH>`), that's the new hash for `dists.ipfs.io`. We also append it to a file called `versions` in the repo root (*not* checked into git).

Next, you should probably:
- This will build any new binaries defined by dist and the website to the `releases` dir, add it to ipfs and patch it into the existing dag for the published `/ipns/dist.ipfs.io`.
- Versions that are already present on the website will be reused, speeding up the build.
- Updated CID (`<NEW_HASH>`) will be printed at the end. That's the new hash for `dists.ipfs.io`. We also append it to a file called `versions` in the repo root (*not* checked into git).

1. Load the dists website in your browser to make sure everything looks right: `http://127.0.0.1:8080/ipfs/<NEW_HASH>`.
2. Compare `<NEW_HASH>` with the current `dists.ipfs.io` to make sure nothing is amiss: `ipfs object diff /ipns/dist.ipfs.io /ipfs/<NEW_HASH>`
After the local build is done, make a quick inspection:

If all looks well, **pin the hash using pinbot** (#ipfs-pinbot on Freenode, ask someone if you don't have permission to do so).
2. Load the dists website in your browser to make sure everything looks right: `http://localhost:8080/ipfs/<NEW_HASH>`.
3. Compare `<NEW_HASH>` with the current `dists.ipfs.io` to make sure nothing is amiss: `ipfs object diff /ipns/dist.ipfs.io /ipfs/<NEW_HASH>`

Finally,

1. Commit your changes and make a PR. Specifically, the changes to `dists/<dist>/versions` and `dists/<dist>/current`.
2. Make a PR with an edit on [protocol/infra](https://github.com/protocol/infra/blob/master/dns/config/dist.ipfs.io.yaml) with the hash you got from `make publish` and a link to the PR above.
2. Wait for [Github Action](https://github.com/ipfs/distributions/actions/) on your PR to build **signed** binaries. `<NEW_SIGNED_HASH>` will be different than one from local build.
3. Make a PR with an edit on [protocol/infra](https://github.com/protocol/infra/blob/master/dns/config/dist.ipfs.io.yaml) with `<NEW_SIGNED_HASH>` you got from the Github Action output and a link to the PR above.
- TODO: this step may be automated in the future - see the [discussion](https://github.com/ipfs/distributions/issues/372).

If you have permission, you can just merge the PR, update the DNS, and then immediately, close the issue on ipfs/infrastructure. Ping someone on IRC.

Expand Down Expand Up @@ -194,6 +207,16 @@ So for example, if we had `<dist>` `go-ipfs` and `fs-repo-migrations`, we might

We call this the **distribution index**, the listing of all distributions, their versions, and platform assets.

### Notes on reproducible builds

Running `./dockerized make publish` will produce binaries using the same
runtime as CI. The main difference between local build and official CI one is
signing step on platforms such as `darwin` (macOS).

Signatures are attached at the end of macOS binaries, which means
`*_darwin-*.tar.gz` produced by CI will have additional bytes when compared
with local build.
lidel marked this conversation as resolved.
Show resolved Hide resolved

## Contribute

Issues and PRs welcome! Please [check out the issues](https://github.com/ipfs/distributions/issues).
Expand Down
6 changes: 3 additions & 3 deletions build-go.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export GOPATH
# Always use go modules
export GO111MODULE=on

DIST_PATH=${DIST_PATH:-/ipns/dist.ipfs.io}
DIST_PATH=$(ipfs resolve "$DIST_PATH")
# Content path to use when looking for pre-existing release data
DIST_ROOT=$(ipfs resolve "${DIST_ROOT:-/ipns/dist.ipfs.io}")

# normalize umask
umask 022
Expand Down Expand Up @@ -334,7 +334,7 @@ function startGoBuilds() {
fi

if [ -z "$existing" ]; then
existing="$DIST_PATH"
existing="$DIST_ROOT"
fi

echo "comparing $versions with $existing/$distname/versions"
Expand Down
2 changes: 1 addition & 1 deletion deps-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ if [ "$failed" = true ]; then
fi

echo "npm install"
exec npm install --no-audit --progress=false
exec npm ci --prefer-offline --no-audit --progress=false
7 changes: 5 additions & 2 deletions dockerized
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ set -euxo pipefail
docker pull ubuntu:20.04

# CACHEBUST means this will apply the updates once a day
docker build . -t distributions --build-arg CACHEBUST=`date --iso-8601=date` --build-arg USER_UID=$(id -u "$USER")
docker build . -t distributions \
--build-arg CACHEBUST=`date --iso-8601=date` \
--build-arg USER_UID=$(id -u "$USER") \
--build-arg GO_IPFS_VER=${GO_IPFS_VER:-$(curl -s https://dist.ipfs.io/go-ipfs/versions | tail -n 1)} # match http api client version on CI
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ this controls the version of the CLI client inside ./dockerized build, which talks to go-ipfs running outside on the host. Here, we ensure it uses the same version (with fallback to the latest one if GO_IPFS_VER is undefined).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this comment live in the source file?

Copy link
Member Author

@lidel lidel Aug 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the comment on the very right is enough, more context can be found on this PR.


# We use host networking as the build process assumes a fairly long-lived ipfs
# node has the CIDs (we give them to the collab cluster to pin)
docker run --rm -it --network host -v `pwd`:/build distributions "$@"
docker run --rm -i --network host -e DIST_ROOT -v `pwd`:/build distributions "$@"
14 changes: 14 additions & 0 deletions scripts/ci/github-preview-link.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e

PREVIEW_URL="https://dweb.link$CONTENT_PATH"
API_PARAMS=$(jq --monochrome-output --null-input \
--arg state "success" \
--arg target_url "$PREVIEW_URL" \
--arg description "Preview updated website on IPFS" \
--arg context "Preview is ready" \
'{ state: $state, target_url: $target_url, description: $description, context: $context }' )
curl --output /dev/null --silent --show-error \
-X POST -H "Authorization: Bearer $GITHUB_TOKEN" -H 'Content-Type: application/json' \
--data "$API_PARAMS" 'https://api.github.com/repos/ipfs/distributions/statuses/${GIT_REVISION}'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ we could replace this script with JS call and actions/github-script but I don't want to sink any more time into refactoring this PR ;)

echo "Pinned to IPFS - $PREVIEW_URL"
28 changes: 28 additions & 0 deletions scripts/ci/pin-to-cluster.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -e

echo "::group::pin add"
ipfs-cluster-ctl --enc=json \
--host "/dnsaddr/ipfs-websites.collab.ipfscluster.io" \
--basic-auth "${CLUSTER_USER}:${CLUSTER_PASSWORD}" \
pin add \
--name "${PIN_NAME}" \
--no-status $PIN_ADD_EXTRA_ARGS \
"$PIN_CID"
echo "::endgroup::"

echo "::group::waiting until pinned"
while true; do
ipfs-cluster-ctl --enc=json \
--host "/dnsaddr/ipfs-websites.collab.ipfscluster.io" \
--basic-auth "${CLUSTER_USER}:${CLUSTER_PASSWORD}" \
status "$PIN_CID" | tee cluster-pin-status
if [[ $(jq '.peer_map[].status' cluster-pin-status | grep '"pinned"' | wc -l) -ge 2 ]]; then
echo "Got 2 pin confirmations, finishing the workflow"
Comment on lines +20 to +21
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ to make CI faster and avoid hanging forever if parts of cluster are out of sync, cluster pinning is considered "done" after we have 2 copies.

Here, I do it manually in userland, but filled ipfs-cluster/ipfs-cluster#1427 to add this sort of thing to ipfs-cluster-ctl CLI tool.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above: when do we decide to put this in the PR vs. the code itself? Do folks tend to do a git blame and look for comments to find more context?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I often use git blame, but in this specific case those comments are provided mostly to make PR review easier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I'll defer to the team for the standard. I was bringing this up because it was hard for me to imagine a case where a comment is useful during PR where it isn't also useful 6 months in the future when looking at the code with fresh eyes. Anyways, sounds good.

break
else
echo "(sleeping for 15 seconds)"
sleep 15
fi
done
echo "::endgroup::"
40 changes: 40 additions & 0 deletions scripts/ci/setup-ipfs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -e

echo "::group::Install go-ipfs and ipfs-cluster-ctl"
curl -s https://dist.ipfs.io/go-ipfs/${GO_IPFS_VER}/go-ipfs_${GO_IPFS_VER}_linux-amd64.tar.gz | sudo tar vzx -C /usr/local/bin/ go-ipfs/ipfs --strip-components=1
curl -s https://dist.ipfs.io/ipfs-cluster-ctl/${CLUSTER_CTL_VER}/ipfs-cluster-ctl_${CLUSTER_CTL_VER}_linux-amd64.tar.gz | sudo tar vzx -C /usr/local/bin/ ipfs-cluster-ctl/ipfs-cluster-ctl --strip-components=1
echo "::endgroup::"

# fix resolv - DNS provided by Github is unreliable for DNSLik/dnsaddr
sudo sed -i -e 's/nameserver 127.0.0.*/nameserver 1.1.1.1/g' /etc/resolv.conf
Comment on lines +9 to +10
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ This was pretty annoying. Entire build failed because Githubs DNS proxying cache at 127.0.0.* randomly timeouted while resolving _dnslink or _dnsaddr. Switching to Cloudflare here make the problem go away.


# QUIC perf: https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
sudo sysctl -w net.core.rmem_max=2500000

# init ipfs
echo "::group::Set up IPFS daemon"
ipfs init --profile flatfs,server,test,lowpower
# make flatfs async for faster ci
new_config=$( jq '.Datastore.Spec.mounts[0].child.sync = false' ~/.ipfs/config) && echo "${new_config}" > ~/.ipfs/config
# restore deterministic port (changed by test profile)
ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5001"
# wait for ipfs daemon
ipfs daemon --enable-gc=false & while (! ipfs id --api "/ip4/127.0.0.1/tcp/5001"); do sleep 1; done
echo "::endgroup::"


echo "::group::Preconnect to cluster peers"
echo '-> preconnect to cluster peers'
ipfs-cluster-ctl --enc=json \
--host "/dnsaddr/ipfs-websites.collab.ipfscluster.io" \
--basic-auth "${CLUSTER_USER}:${CLUSTER_PASSWORD}" \
peers ls > cluster-peers-ls
for maddr in $(jq -r '.[].ipfs.addresses[]?' cluster-peers-ls); do
ipfs swarm connect "$maddr" || continue
done
echo '-> manual connect to cluster.ipfs.io'
ipfs swarm connect /dnsaddr/cluster.ipfs.io
echo '-> list swarm peers'
ipfs swarm peers
echo "::endgroup::"
Loading