diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39f87825d..6e5e6bc2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,39 +91,9 @@ jobs: path: ipfs-webui_${{ github.sha }}.car if-no-files-found: error - # Instructions for this are at https://github.com/ipfs/ipfs-webui/issues/2161#issuecomment-1836835068 - - name: Pin to web3.storage with w3cli - id: pin-w3up - # only pin for main commits - if: github.repository == 'ipfs/ipfs-webui' && github.ref == 'refs/heads/main' - continue-on-error: true - run: | - # ensure whoami - npx -y --package=@web3-storage/w3cli@latest -- w3 whoami - - # convert base64 env var to file - echo $W3CLI_SPACE_DELEGATION_PROOF_BASE64_STRING | base64 -d > ipfs-webui-ci-space.ucan.proof - - # Add space - export W3CLI_SPACE_DID=$(npx -y --package=@web3-storage/w3cli@latest -- w3 space add ipfs-webui-ci-space.ucan.proof) - - # use space - npx -y --package=@web3-storage/w3cli@latest -- w3 space use $W3CLI_SPACE_DID - - # upload car - npx -y --package=@web3-storage/w3cli@latest -- w3 up --no-wrap -c ipfs-webui_${{ github.sha }}.car - env: - W3_STORE_NAME: ipfs-webui-ci - W3_AGENT_DID: ${{ secrets.W3_AGENT_DID }} - # set w3cli principal from https://github.com/web3-storage/w3cli#w3_principal - W3_PRINCIPAL: ${{ secrets.W3_AGENT_PRINCIPAL }} - W3CLI_SPACE_DELEGATION_PROOF_BASE64_STRING: ${{ secrets.W3CLI_SPACE_DELEGATION_PROOF_BASE64_STRING }} - - name: Pin to ipfs-websites.collab.ipfscluster.io - id: pin-cluster # only pin for main commits if: github.repository == 'ipfs/ipfs-webui' && github.ref == 'refs/heads/main' - continue-on-error: true timeout-minutes: 5 # pin should takes a few seconds, 5 min is a hard ceiling run: | echo "Avoid doing initial upload to am6-1 node with experimental pebble datastore" @@ -159,14 +129,6 @@ jobs: CLUSTER_PASSWORD: ${{ secrets.CLUSTER_PASSWORD }} PIN_NAME: "ipfs-webui@${{ github.sha }}" - - name: Fail job due to pinning failure - # only fail if pinning failed for main commits - if: github.ref == 'refs/heads/main' && steps.pin-w3up.outcome == 'failure' && steps.pin-cluster.outcome == 'failure' - uses: actions/github-script@v8 - with: - script: | - core.setFailed('Pinning did not succeed') - # dnslink-dnsimple requires go - uses: actions/setup-go@v6 if: github.repository == 'ipfs/ipfs-webui' && github.ref == 'refs/heads/main' @@ -198,59 +160,6 @@ jobs: force_orphan: true full_commit_message: 'ipfs-webui@${{ github.sha }} /ipfs/${{ steps.ipfs.outputs.cid }}' - # production dnslink is updated on release (workflow_dispatch on main) - - run: dnslink-dnsimple --domain ipfs.tech --record _dnslink.webui --link /ipfs/${{ steps.ipfs.outputs.cid }} - if: github.repository == 'ipfs/ipfs-webui' && github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' - env: - DNSIMPLE_TOKEN: ${{ secrets.DNSIMPLE_TOKEN }} - - # HTTP hosting for https://webui.ipfs.io (GitHub Pages, deployed via Actions). - # Gated exactly like the production dnslink update in publishPreview, so the Pages - # site and DNSLink always publish the same content in the same run. Bytes come - # from the CAR artifact (the only unconditional per-run copy of build/; the - # build artifact is skipped on cache hits), so served content matches the pinned CID. - deployPages: - name: deploy webui.ipfs.io (GitHub Pages) - needs: publishPreview - if: github.repository == 'ipfs/ipfs-webui' && github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - concurrency: - group: pages-prod - cancel-in-progress: false - steps: - - name: Download CAR artifact - uses: actions/download-artifact@v7 - with: - name: ipfs-webui_${{ github.sha }}.car - - - uses: ipfs/download-ipfs-distribution-action@v1 - with: - name: kubo - - - name: Unpack CAR into build/ - run: | - ipfs init - ipfs dag import --pin-roots=false ipfs-webui_${{ github.sha }}.car - ipfs get ${{ needs.publishPreview.outputs.cid }} -o build - test -f build/index.html - # fail loudly if the unpacked tree does not match the CID that was pinned and DNSLinked - [ "$(ipfs add --cid-version 1 -Q -r -n ./build)" = "${{ needs.publishPreview.outputs.cid }}" ] - - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v5 - with: - path: build - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 - test-e2e: name: 'test:e2e' uses: ./.github/workflows/test-e2e.yml @@ -320,8 +229,10 @@ jobs: exit 1 # creates a GitHub Release with changelog and CAR file when ci.yml is manually - # dispatched on main; the release commit lands with [skip ci], so no follow-up - # workflow run is triggered by it + # dispatched on main. Publishing the release triggers deploy-release.yml, which + # deploys webui.ipfs.io (GitHub Pages) and updates the production dnslink, so a + # failed or skipped release never changes what users see. The release commit + # itself lands with [skip ci] and triggers nothing. release: name: 'Run semantic release' runs-on: ubuntu-latest diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml new file mode 100644 index 000000000..4fc6aa0f1 --- /dev/null +++ b/.github/workflows/deploy-release.yml @@ -0,0 +1,97 @@ +name: deploy-release + +# Publishes a tagged release to https://webui.ipfs.io: deploys the CAR file +# attached to the GitHub Release to GitHub Pages, then points the production +# dnslink at the same CID. Triggered automatically when semantic-release +# (ci.yml) publishes a release, so a failed or skipped release never changes +# what users see. Can also be dispatched manually with any existing release +# tag to redeploy the site or roll back to an older release. +# +# NOTE: the automatic trigger relies on ci.yml running semantic-release with a +# bot PAT (secrets.GH_TOKEN). Events created with the workflow-scoped +# GITHUB_TOKEN do not trigger other workflows, so switching that secret to the +# default token would silently stop these deploys. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'release tag to (re)deploy, e.g. v4.13.0' + required: true + type: string + +# queue deploys in order, never cancel one mid-flight +concurrency: + group: deploy-release + cancel-in-progress: false + +env: + RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }} + +jobs: + pages: + name: deploy webui.ipfs.io (GitHub Pages) + if: github.repository == 'ipfs/ipfs-webui' + runs-on: ubuntu-latest + outputs: + cid: ${{ steps.import.outputs.cid }} + permissions: + contents: read # download the release asset + pages: write + id-token: write # OIDC for deploy-pages + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Download CAR from the release + run: gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.car' --output release.car + env: + GH_TOKEN: ${{ github.token }} + + - uses: ipfs/download-ipfs-distribution-action@v1 + with: + name: kubo + + - name: Unpack CAR into build/ + id: import + run: | + ipfs init + root_cid=$(ipfs dag import release.car | awk '/^Pinned root/ { print $3 }') + test -n "$root_cid" + ipfs get "$root_cid" -o build + test -f build/index.html + # fail loudly if the unpacked tree does not match the released DAG + [ "$(ipfs add --cid-version 1 -Q -r -n ./build)" = "$root_cid" ] + echo "cid=$root_cid" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-pages-artifact@v5 + with: + path: build + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 + + dnslink: + name: update production dnslink + needs: pages + if: github.repository == 'ipfs/ipfs-webui' + runs-on: ubuntu-latest + environment: Deploy # holds DNSIMPLE_TOKEN + permissions: + contents: read + steps: + - uses: actions/checkout@v6 # only for .tool-versions + + - uses: actions/setup-go@v6 + with: + go-version-file: '.tool-versions' + + - run: go install github.com/ipfs/dnslink-dnsimple@v0.1.0 + + - name: Point _dnslink.webui at the released CID + run: dnslink-dnsimple --domain ipfs.tech --record _dnslink.webui --link "/ipfs/${{ needs.pages.outputs.cid }}" + env: + DNSIMPLE_TOKEN: ${{ secrets.DNSIMPLE_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..41267e5a2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# AGENTS.md + +Guidance for coding agents working in this repository. Humans may find it useful too. + +## Build and test + +- `npm ci` installs dependencies, `npm run build` builds into `build/` (create-react-app via react-app-rewired-esm) +- `npx jest ` runs a single unit test file, `npm run test:unit` the suite +- `npm run eslint` and `npm run typecheck` must pass; CI treats warnings as errors +- Node.js version is pinned in `.tool-versions` +- translations live in `public/locales` and are managed on Transifex; see `docs/LOCALIZATION.md` before touching them + +## Commit conventions + +- follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/); semantic-release derives version bumps and the changelog from commit titles on `main` (config in `package.json` under `release`) +- `chore:`, `docs:`, and `test:` commits trigger patch releases; use the `no-release` scope for commits that should not be released + +## Release and deployment flow + +The release process for humans is described in `docs/RELEASING.md`. The machinery: + +1. Every merge to `main` (`ci.yml`, job `publishPreview`): the build is added to IPFS and pinned, exported as a CAR file kept as a run artifact, the dev DNSLink (`_dnslink.dev.webui`, zone `ipfs.tech`) is updated, and the build is force-pushed to the `ipfs/dev.webui.ipfs.io` repo, which GitHub Pages serves as https://dev.webui.ipfs.io. +2. A maintainer manually dispatches `ci.yml` on `main`: if release-worthy commits exist, semantic-release publishes a GitHub Release with the CID in the notes and the CAR file attached. That CAR is the canonical source of truth for the release; everything user-facing deploys from it. +3. Publishing the release triggers `deploy-release.yml`: it downloads the CAR from the release, verifies the root CID, deploys the content to https://webui.ipfs.io (GitHub Pages, this repo) and points the production DNSLink (`_dnslink.webui`) at the same CID. A failed or skipped release never changes https://webui.ipfs.io. +4. `deploy-release.yml` can be dispatched manually with a release tag to redeploy or roll back https://webui.ipfs.io. + +Forks: pinning and publishing steps are gated on `github.repository == 'ipfs/ipfs-webui'`, so CI in forks skips them instead of failing on missing secrets. + +## Hosting notes + +- the app is a static, hash-routed build with relative asset paths; it works from GitHub Pages, any IPFS gateway path, and as `ipns://webui.ipfs.io` via IPFS Companion +- both hostnames are CNAMEs to `ipfs.github.io` (DNS-only) in the `ipfs.io` zone; the `_dnslink.*` TXT records are load-bearing for IPFS-native access and must never be removed +- Kubo and IPFS Desktop bundle release CIDs directly (see `docs/RELEASING.md`), independent of the hosted sites diff --git a/README.md b/README.md index f5ff539cc..d0689c234 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ This repository follows the IPFS [Code of Conduct](https://github.com/ipfs/commu > [!NOTE] > This project is maintained by the [Shipyard](https://ipshipyard.com/) team. > -> [Releasing a new version](docs/RELEASING.md) +> [Releasing a new version](docs/RELEASING.md): every merge to `main` is deployed to https://dev.webui.ipfs.io, and every tagged release to https://webui.ipfs.io. ## License diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 9b7d242d6..7aecb57c1 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -6,8 +6,14 @@ 2. If UI is materially different, update screenshots in `README.md` and on [docs.ipfs.tech](https://docs.ipfs.tech/how-to/command-line-quick-start/). -3. Manually dispatch the [ci.yml](https://github.com/ipfs/ipfs-webui/actions/workflows/ci.yml) workflow on `main` branch. This will create a new release. +3. Manually dispatch the [ci.yml](https://github.com/ipfs/ipfs-webui/actions/workflows/ci.yml) workflow on `main` branch. If there are release-worthy commits, semantic-release creates a new GitHub Release with the changelog, the build CID in the notes, and the build CAR file attached. -4. If the release is good enough for LTS, update the CID at projects that use ipfs-webui by submitting PRs: +4. Publishing the release automatically triggers [deploy-release.yml](https://github.com/ipfs/ipfs-webui/actions/workflows/deploy-release.yml), which downloads the CAR from the release, deploys its content to https://webui.ipfs.io (GitHub Pages) and points the production DNSLink at the same CID. Check that both of its jobs are green. If the dispatched run fails, or creates no release, https://webui.ipfs.io stays untouched. + +5. If the release is good enough for LTS, update the CID at projects that use ipfs-webui by submitting PRs: - **Kubo**: https://github.com/ipfs/kubo/blob/master/core/corehttp/webui.go#L4 - **IPFS Desktop**: https://github.com/ipfs/ipfs-desktop/blob/main/package.json#L20 + +## Redeploying or rolling back webui.ipfs.io + +Manually dispatch [deploy-release.yml](https://github.com/ipfs/ipfs-webui/actions/workflows/deploy-release.yml) with any existing release tag (e.g. `v4.13.0`). It deploys that release's CAR file to GitHub Pages and DNSLink; this is the fastest way to recover from a bad release.