Capture a GitHub Actions artifact's identity, digest, expiry and verified external copy before retention removes it. Since September 24, an expired artifact disappears from GitHub's run summary and artifact REST endpoints, so a release-evidence system cannot reconstruct a durable record from the provider listing after the deadline.
GitHub's change removes an ambiguity. An artifact marked expired previously remained visible even though the underlying files had already been deleted. Now the interface and artifact endpoints reflect the storage truth: the expired record is gone. (GitHub Changelog)
That is sensible product behavior. It also means a release pipeline that treats GitHub's artifact list as its permanent evidence index has no durable index.
Expiry now removes the provider record
An expired artifact is no longer displayed on the workflow-run summary. GitHub's repository list-artifacts and get-artifact endpoints no longer return it either. The change affects display and API visibility, not retention settings or billing.
GitHub says the run logs can still identify which artifacts a workflow produced after expiry. That recovery route has its own horizon. Starting October 1, 2026, checks, workflow runs and statuses will follow the same Actions retention setting already used for artifacts and logs. The default is 90 days, public repositories have a 90-day maximum for this expanded metadata, and raising retention cannot restore records already evicted. (Retention change)
The operating conclusion is narrow: use GitHub as the live source while the record exists. If a release, audit or safety obligation lasts longer, retain a separate manifest and payload copy before expiry.

Write the manifest while the record exists
A useful manifest binds the payload to the exact producer and the retention decision. GitHub's artifact REST representation provides the core provider fields: artifact ID, name, size, digest, creation and expiry timestamps, plus the workflow-run ID and head SHA. (Artifacts REST API)
Retain at least:
| Record | Minimum fields |
|---|---|
| Producer | repository, workflow path, ref, head SHA, run ID, run attempt |
| Artifact | provider artifact ID, name, size, digest, created time, expiry time |
| Policy | evidence owner, required-through time, external-copy decision |
| Observation | observed time, API version, request receipt or error |
| External copy | governed object URI, copied digest, read-back time |
| Provenance | attestation reference, when one exists |
Do not use an artifact name as identity. Names can repeat across runs. The manifest needs the provider artifact ID and the producing run tuple so a later reviewer can distinguish two files called release-evidence.
The reference builder makes the policy comparison explicit:
const manifest = createEvidenceManifest({
repository: "acme/payments",
workflowPath: ".github/workflows/release.yml",
ref: "refs/heads/main",
headSha: releaseSha,
runId: run.id,
runAttempt: run.run_attempt,
artifact: {
id: artifact.id,
name: artifact.name,
sizeBytes: artifact.size_in_bytes,
digest: artifact.digest,
createdAt: artifact.created_at,
expiresAt: artifact.expires_at,
},
observedAt: new Date().toISOString(),
requiredThrough: policy.releaseEvidenceThrough,
});If requiredThrough is later than expiresAt, the manifest sets externalCopyRequired to true. This is a DVNC implementation recommendation, not a field GitHub supplies. The business owner decides how long the evidence must remain available; the collector turns that decision into a machine-checkable requirement.
Copy bytes before expiry and verify them
Copying the ZIP is only an accepted intent. The retained outcome is a readable object whose digest matches the digest bound into the manifest.
Use this sequence:
- Read the artifact record and freeze its identity fields.
- Download the payload while the provider still makes it available.
- Write the payload to the approved evidence store under an immutable key.
- Read the stored object back through the path a reviewer will use.
- Recalculate the digest and compare it with the manifest.
- Record the object URI, digest and observation time.
- Only then mark the copy retained.
GitHub's download endpoint documents 410 Gone as a possible terminal. A collector that reaches that response without a prior verified copy has not archived the evidence. It has discovered that its opportunity to do so ended.
The destination needs its own controls. Use retention lock or write-once policy where the obligation requires it, separate delete authority from the producing workflow, and log every read and deletion. An object URI in a manifest is not proof that the object remains readable.
This is the same distinction that applies to the complete enumeration of GitHub Actions runs: provider acceptance and a finished request loop are not coverage. A retained claim needs reconciled records.
Keep provenance separate from availability
An artifact attestation can prove build provenance. It does not preserve the artifact bytes.
GitHub's current workflow uses actions/attest@v4 and binds an attestation to a subject path or a subject name and digest. The documented binary example requires id-token: write, contents: read and attestations: write. (Artifact attestations)
That gives a reviewer a signed provenance statement about the subject. The inference for an evidence system is important: a provenance statement and an available payload answer different questions.
- Attestation: who or what produced the subject under which workflow identity?
- Payload copy: can the reviewer still retrieve the exact bytes?
- Manifest: which producer, artifact, policy, copy and observations belong together?
Do not let the presence of an attestation promote a missing payload to archived. Keep the state incomplete until the retained object is independently readable and its digest matches.
Run logs are also supporting evidence, not the archive. They can help recover the artifact name or show that an upload step ran. They do not provide the expired bytes, and their own retention horizon is becoming more explicit. Export a log bundle if the review obligation needs it, bind its digest into the manifest, and still keep it separate from the artifact payload.
Reconcile six states
The evidence reader should return a state, not a boolean.
const result = reconcileEvidence({
manifest,
providerArtifact,
archiveObservation,
observedAt: new Date().toISOString(),
});Use six outcomes:
| State | Meaning | Safe next action |
|---|---|---|
observed | GitHub still returns the matching artifact ID and digest | Continue scheduled verification |
archived | Provider record is absent after expiry; verified copy matches | Retain the reconciliation receipt |
expired_by_policy | Evidence duty ended before provider expiry | Close under the approved policy |
uncertain | Record vanished early or expected archive was not observed | Stop promotion and investigate |
incomplete | Policy required a copy, but none was recorded | Fail the evidence gate |
divergent | Provider or archive identity or digest differs | Quarantine and investigate |

This classification prevents two opposite mistakes. The first is declaring every missing provider record a failure even when the approved evidence duty ended. The second is calling every missing record an expected expiry even when it vanished early or the external copy was never verified.
Keep observedAt on every result. Provider state can change after a successful read, so an old receipt proves only what was observed at that time.
Release the evidence gate
The dependency-free reference artifact for this article passed 16 of 16 local tests. It covers manifest versioning, retention-policy comparison, digest and timestamp validation, run-attempt validation, matching provider reads, provider ID and digest divergence, disappearance before expiry, verified archives, archive divergence, missing copies, attestation-only failure, policy-complete expiry and unobserved archives.
The artifact did not call GitHub or an object store. It is a reference implementation, not proof that any production archive is complete.
Before putting this method under a coding-agent or software-release workflow, prove:
- the collector records artifact ID, digest, expiry, run ID, run attempt and head SHA from the live endpoint;
- the policy owner supplies a required-through time rather than accepting the provider default by accident;
- a copy required beyond expiry is downloaded before the endpoint returns
410 Gone; - the stored bytes are read back and independently hashed;
- the attestation reference cannot substitute for missing payload bytes;
- a missing provider record before expected expiry stays
uncertain; - a digest mismatch stops release promotion;
- manifest, copy and reconciliation receipts are immutable under the workflow's credential; and
- expiry and deletion tests run against the actual storage policies, not only local fixtures.
The repository-instructions release process pins a coding agent's effective runtime before promotion. Apply the same discipline to its evidence: a release is not reproducible if the retained record outlives neither the provider's listing nor the business decision it was meant to support.
FAQ
Why did my expired GitHub Actions artifact disappear?
GitHub stopped displaying expired artifacts in workflow-run summaries and stopped returning them from the repository list-artifacts and get-artifact endpoints on September 24, 2026. The underlying files were already removed at expiry under the prior behavior; the expired pill had merely remained visible.
Can I retrieve an expired artifact through the REST API?
No. GitHub no longer returns expired artifacts from its list and get endpoints, and the download endpoint can return 410 Gone. If the bytes must live longer than GitHub retention, copy and verify them before expiry.
Do artifact attestations preserve the artifact file?
No. An attestation establishes provenance for a subject. Preserve the payload separately and reconcile its digest with the manifest; an attestation alone is not a readable archive.
How long does GitHub keep Actions artifacts and logs?
GitHub's default Actions retention is 90 days, but repository, organization and enterprise settings can change the configured period within plan and policy caps. Starting October 1, 2026, checks, workflow runs and statuses follow that same retention setting. Record the actual expiry timestamp instead of assuming the default.








