Skip to content
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
119 changes: 72 additions & 47 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
name: Publish

on:
deployment:
workflow_run:
workflows:
- Release Please
types:
- completed
workflow_dispatch:
inputs:
recovery_task:
description: Exact one-cycle recovery task.
required: true
type: string
release_commit:
description: Immutable v0.1.1 release commit.
required: true
type: string
release_tag:
description: Immutable release tag.
required: true
type: string
release_run_id:
description: Successful Release Please run ID.
required: true
type: string
release_run_attempt:
description: Successful Release Please run attempt.
required: true
type: string
source_publish_run_id:
description: Failed Publish run whose evidence is being recovered.
required: true
type: string
source_publish_run_attempt:
description: Failed Publish run attempt.
required: true
type: string

permissions:
actions: read
Expand All @@ -26,17 +55,21 @@ jobs:
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main') ||
(github.event_name == 'deployment' &&
github.event.deployment.task == 'npm-publish-recovery' &&
github.event.deployment.environment == 'npm' &&
github.event.deployment.ref == 'v0.1.1' &&
github.event.deployment.sha == 'c98b514227858cd183c781270a7f78f65b577e82'))
(github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/tags/v0.1.1' &&
inputs.recovery_task == 'npm-publish-recovery' &&
inputs.release_commit == 'c98b514227858cd183c781270a7f78f65b577e82' &&
inputs.release_tag == 'v0.1.1' &&
inputs.release_run_id == '30469181724' &&
inputs.release_run_attempt == '1' &&
inputs.source_publish_run_id == '30471665743' &&
inputs.source_publish_run_attempt == '1'))
runs-on: ubuntu-latest
timeout-minutes: 30
env:
SOURCE_RELEASE_COMMIT: ${{ github.event_name == 'deployment' && 'c98b514227858cd183c781270a7f78f65b577e82' || github.event.workflow_run.head_sha }}
SOURCE_RELEASE_RUN_ATTEMPT: ${{ github.event_name == 'deployment' && '1' || github.event.workflow_run.run_attempt }}
SOURCE_RELEASE_RUN_ID: ${{ github.event_name == 'deployment' && '30469181724' || github.event.workflow_run.id }}
SOURCE_RELEASE_COMMIT: ${{ github.event_name == 'workflow_dispatch' && inputs.release_commit || github.event.workflow_run.head_sha }}
SOURCE_RELEASE_RUN_ATTEMPT: ${{ github.event_name == 'workflow_dispatch' && inputs.release_run_attempt || github.event.workflow_run.run_attempt }}
SOURCE_RELEASE_RUN_ID: ${{ github.event_name == 'workflow_dispatch' && inputs.release_run_id || github.event.workflow_run.id }}
outputs:
artifact-name: ${{ steps.artifact-name.outputs.name }}
dist-tag: ${{ steps.version.outputs.dist-tag }}
Expand All @@ -50,27 +83,24 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event_name == 'deployment' && github.workflow_sha || github.sha }}
- name: Validate the exact tag deployment recovery
if: github.event_name == 'deployment'
ref: ${{ github.event_name == 'workflow_dispatch' && github.workflow_sha || github.sha }}
- name: Validate the exact tag workflow dispatch recovery
if: github.event_name == 'workflow_dispatch'
env:
ACTOR: ${{ github.actor }}
CHANGED_FILES: ${{ runner.temp }}/publish-recovery-files
CONTROL_COMMIT: ${{ github.workflow_sha }}
DEPLOYMENT_CREATOR: ${{ github.event.deployment.creator.login }}
DEPLOYMENT_ENVIRONMENT: ${{ github.event.deployment.environment }}
DEPLOYMENT_ID: ${{ github.event.deployment.id }}
DEPLOYMENT_REF: ${{ github.event.deployment.ref }}
DEPLOYMENT_RELEASE_COMMIT: ${{ github.event.deployment.payload.release_commit }}
DEPLOYMENT_RELEASE_TAG: ${{ github.event.deployment.payload.release_tag }}
DEPLOYMENT_SHA: ${{ github.event.deployment.sha }}
DEPLOYMENT_SOURCE_RUN_ATTEMPT: ${{ github.event.deployment.payload.source_run_attempt }}
DEPLOYMENT_SOURCE_RUN_ID: ${{ github.event.deployment.payload.source_run_id }}
DEPLOYMENT_TASK: ${{ github.event.deployment.task }}
EVENT_NAME: ${{ github.event_name }}
EVENT_REF: ${{ github.ref }}
EVENT_SHA: ${{ github.sha }}
MAIN_COMMIT: ${{ github.workflow_sha }}
RELEASE_COMMIT: ${{ inputs.release_commit }}
RELEASE_TAG: ${{ inputs.release_tag }}
RELEASE_RUN_ATTEMPT: ${{ inputs.release_run_attempt }}
RELEASE_RUN_ID: ${{ inputs.release_run_id }}
SOURCE_PUBLISH_RUN_ATTEMPT: ${{ inputs.source_publish_run_attempt }}
SOURCE_PUBLISH_RUN_ID: ${{ inputs.source_publish_run_id }}
TASK: ${{ inputs.recovery_task }}
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }}
shell: bash
Expand All @@ -91,50 +121,45 @@ jobs:
MAIN_COMMIT="$MAIN_COMMIT" CONTROL_FIRST_PARENT="$CONTROL_FIRST_PARENT" \
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import { validatePublishDeploymentRecoveryTrigger } from "./scripts/release-workflow-validation.mjs";
import { validatePublishWorkflowDispatchRecoveryTrigger } from "./scripts/release-workflow-validation.mjs";

validatePublishDeploymentRecoveryTrigger({
validatePublishWorkflowDispatchRecoveryTrigger({
actor: process.env.ACTOR,
changedFiles: readFileSync(process.env.CHANGED_FILES, "utf8")
.split("\n")
.filter((file) => file !== ""),
controlCommit: process.env.CONTROL_COMMIT,
controlFirstParent: process.env.CONTROL_FIRST_PARENT,
deploymentCreator: process.env.DEPLOYMENT_CREATOR,
deploymentEnvironment: process.env.DEPLOYMENT_ENVIRONMENT,
deploymentId: Number(process.env.DEPLOYMENT_ID),
deploymentRef: process.env.DEPLOYMENT_REF,
deploymentReleaseCommit: process.env.DEPLOYMENT_RELEASE_COMMIT,
deploymentReleaseTag: process.env.DEPLOYMENT_RELEASE_TAG,
deploymentSha: process.env.DEPLOYMENT_SHA,
deploymentSourceRunAttempt: Number(
process.env.DEPLOYMENT_SOURCE_RUN_ATTEMPT,
),
deploymentSourceRunId: Number(process.env.DEPLOYMENT_SOURCE_RUN_ID),
deploymentTask: process.env.DEPLOYMENT_TASK,
eventName: process.env.EVENT_NAME,
eventRef: process.env.EVENT_REF,
eventSha: process.env.EVENT_SHA,
mainCommit: process.env.MAIN_COMMIT,
releaseCommit: process.env.RELEASE_COMMIT,
releaseTag: process.env.RELEASE_TAG,
sourcePublishRunAttempt: Number(
process.env.SOURCE_PUBLISH_RUN_ATTEMPT,
),
sourcePublishRunId: Number(process.env.SOURCE_PUBLISH_RUN_ID),
sourceReleaseCommit: process.env.SOURCE_RELEASE_COMMIT,
sourceRunAttempt: Number(process.env.SOURCE_RELEASE_RUN_ATTEMPT),
sourceRunId: Number(process.env.SOURCE_RELEASE_RUN_ID),
task: process.env.TASK,
triggeringActor: process.env.TRIGGERING_ACTOR,
workflowRunAttempt: Number(process.env.WORKFLOW_RUN_ATTEMPT),
});
EOF
- name: Validate the prior artifact and bounded live evidence
id: recovery-evidence
if: github.event_name == 'deployment'
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
RECOVERY_ANNOTATIONS: ${{ runner.temp }}/publish-recovery-annotations.json
RECOVERY_ARTIFACTS: ${{ runner.temp }}/publish-recovery-artifacts.json
RECOVERY_JOBS: ${{ runner.temp }}/publish-recovery-jobs.json
RECOVERY_LIVE_LOG: ${{ runner.temp }}/publish-recovery-live.log
RECOVERY_RUN: ${{ runner.temp }}/publish-recovery-run.json
SOURCE_PUBLISH_ATTEMPT: ${{ github.event.deployment.payload.source_run_attempt }}
SOURCE_PUBLISH_RUN_ID: ${{ github.event.deployment.payload.source_run_id }}
SOURCE_PUBLISH_ATTEMPT: ${{ inputs.source_publish_run_attempt }}
SOURCE_PUBLISH_RUN_ID: ${{ inputs.source_publish_run_id }}
shell: bash
run: |
set -euo pipefail
Expand Down Expand Up @@ -211,7 +236,7 @@ jobs:
EXPECTED_REPOSITORY_URL: git+https://github.com/cometapi-dev/cometapi-node.git
EXPECTED_WORKFLOW: Release Please
EXPECTED_WORKFLOW_PATH: .github/workflows/release-please.yml
CONTROL_SHA: ${{ github.event_name == 'deployment' && github.workflow_sha || github.sha }}
CONTROL_SHA: ${{ github.event_name == 'workflow_dispatch' && github.workflow_sha || github.sha }}
EVENT_NAME: ${{ github.event_name }}
RELEASE_RESULT: ${{ runner.temp }}/release-please-result/result.json
SOURCE_RUN_FILE: ${{ runner.temp }}/release-please-source-run.json
Expand All @@ -230,7 +255,7 @@ jobs:
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
case "$EVENT_NAME" in
workflow_run) expected_main="$WORKFLOW_SHA" ;;
deployment) expected_main="$CONTROL_SHA" ;;
workflow_dispatch) expected_main="$CONTROL_SHA" ;;
*) echo "Publish received an unsupported event." >&2; exit 1 ;;
esac
if [[ "$(git rev-parse refs/remotes/origin/main)" != "$expected_main" ]]; then
Expand Down Expand Up @@ -447,7 +472,7 @@ jobs:
environment: live-smoke
steps:
- name: Reuse the successful bounded live smoke
if: github.event_name == 'deployment'
if: github.event_name == 'workflow_dispatch'
env:
REUSE_LIVE_SMOKE: ${{ needs.verify.outputs.reuse-live-smoke }}
shell: bash
Expand All @@ -458,25 +483,25 @@ jobs:
exit 1
fi
- name: Check out the verified release tag
if: github.event_name != 'deployment'
if: github.event_name != 'workflow_dispatch'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ needs.verify.outputs.release-commit }}
- name: Set up Node.js 24
if: github.event_name != 'deployment'
if: github.event_name != 'workflow_dispatch'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: npm
- name: Install locked dependencies
if: github.event_name != 'deployment'
if: github.event_name != 'workflow_dispatch'
run: npm ci
- name: Build the release tag
if: github.event_name != 'deployment'
if: github.event_name != 'workflow_dispatch'
run: npm run build
- name: Run the bounded live smoke
if: github.event_name != 'deployment'
if: github.event_name != 'workflow_dispatch'
env:
COMETAPI_KEY: ${{ secrets.COMETAPI_KEY }}
COMETAPI_LIVE_SMOKE: "1"
Expand Down
41 changes: 33 additions & 8 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,17 +303,42 @@ The repository maintains four independently auditable workflows:
`main` deployment while the protected npm environment accepts only `v*` tags.
No OIDC token or npm mutation occurred in either failed run.

The replacement one-cycle recovery uses a human-created GitHub deployment for
the existing immutable `v0.1.1` tag. It accepts only actor and triggering actor
`tensornull`, exact release commit
`c98b514227858cd183c781270a7f78f65b577e82`, Release Please run
`30469181724` attempt 1, failed Publish run `30471665743` attempt 1, the exact
A first attempted recovery deployment [5667717157](https://github.com/cometapi-dev/cometapi-node/deployments/5667717157)
was intentionally marked failed: the immutable `v0.1.1` tag predates the
temporary deployment trigger, so GitHub found no workflow at that tag and no
runner or npm mutation occurred. The replacement one-cycle recovery uses the
documented `workflow_dispatch` API with `ref=v0.1.1`; GitHub dispatches the
workflow from the reviewed default-branch definition while setting
`GITHUB_REF=refs/tags/v0.1.1`, which satisfies the existing npm tag policy.
The exact request is:

```bash
gh api --method POST \
repos/cometapi-dev/cometapi-node/actions/workflows/publish.yml/dispatches \
--input - <<'JSON'
{
"ref": "v0.1.1",
"inputs": {
"recovery_task": "npm-publish-recovery",
"release_commit": "c98b514227858cd183c781270a7f78f65b577e82",
"release_tag": "v0.1.1",
"release_run_id": "30469181724",
"release_run_attempt": "1",
"source_publish_run_id": "30471665743",
"source_publish_run_attempt": "1"
}
}
JSON
```

The workflow accepts only actor and triggering actor `tensornull`, the exact
release commit and tag, both source run IDs and attempts, the reviewed
first-parent control merge, and the recorded repair files. It revalidates the
successful source verify job, artifact ID and digest, branch-policy failure,
and the log evidence for exactly three sequential live requests. The new tag
run repeats offline package and exact-artifact gates but does not spend another
and log evidence for exactly three sequential live requests. The tag run
repeats offline package and exact-artifact gates but does not spend another
live request budget. The protected npm environment and OIDC gate remain
unchanged. The deployment trigger and exact recovery constants must be removed
unchanged. The dispatch trigger and exact recovery constants must be removed
in the post-release evidence PR; the `runner.temp` isolation remains permanent.

Third-party actions are pinned to full commit SHAs. Workflow permissions remain
Expand Down
Loading